This follows on from my previous post on an error handling mechanism. Prompted by David Ungar to think further on how that mechanism could be made more general and powerful, and less of a one off special exception, this is a description of one way in which certain objects can adjust their behaviour based not only on their delegatees and but also on a ‘perspective’ – a viewpoint bound to a process. It’s a great example I think of the power of Self as a language.
Subjectivity and Us
Normal objects in Self are objective – that is their behaviour depends only on themselves and will be the same in all contexts.
In 1996, Randall B Smith and David Ungar wrote a paper called “A simple and unifying approach to subjective objects” (download as a pdf) in which they described a system called ‘Us’ where objects were subjective rather than objective. The behaviour of Us objects (in technical terms the lookup mechanism for delegation) depended on what perspective the message was sent with.
Objects were to be built up in two planes – by normal delegation and by layering pieces onto the whole. Each message received by the object would be dispatched into that three dimensional space based upon not only message selector but also on an implicit argument to the message – the perspective object.
So two people could look at an object in two different ways – to me it might be a circle, to you a set of slots. Or it might look like a pie chart to me and a table to you. An object would be not so much a concrete thing like a pebble but a ‘figment of its viewers beliefs’ (as the authors quote Alan Kay as saying)
Randall and David didn’t build a complete system this way. I don’t know of anyone who has, although the work done on Classboxes in Smalltalk-80 and Java is a very interesting application of similar ideas to modularisation and there are a number of other interesting papers which cite the Us paper.
I understand Randall and David’s paper correctly, a pure Us implementation would require a check at every lookup, so turning Self into Us would take a lot of effort and changing the VM lookup code if it were to be efficient.
However it might be a useful exercise to build a mechanism to investigate and play with subjectivity, even if that mechanism doesn’t get us all the benefits of a full-blown Us.
This is a thought experiment on some of the Us principles. It’s not an implementation of Us and neither Randall not David should be blamed for its inadequacies! In particular, perspectives in Us are not necessarily bound to a process, nor do they necessarily as I understand it hold true beyond the initial method – that is unlike the mechanism below a perspective wouldn’t stay fixed for the call stack until the original method exits. As well, Us wasn’t envisioned as an being a capacity of Self but as a new language which was a superset of the existing Self language.
On the other hand, as you’ll see below, this is a short and simple experiment!
Mechanism
The mechanism I have in mind looks like this:
To make an object “subjective”, we share the subjective mixin:
o: (|
m* = mixins subjective
|)
Ordinary slots will behave ordinarily:
o: (|
m* = mixins subjective.
x = 1.
|).
o x == 1
To create subjective behaviour, we add layers (with overlapping slots) as parent slots:
o: (|
m* = mixins subjective.
default* = (| hello = ('Hello') |).
french* = (| hello = ('Bonjour') |)
|)
These layers shouldn’t themselves have further parent slots, and they should overlap so that their slots are the same. If we want undefined behaviour we need to do something like (| hello = (|l = lobby| l raiseError) |)
rather than leaving the hello slot out of our layer.
Once we have done this, sending our object the message ‘hello’ will get us the string ‘Hello’. However, if we do:
[o hello] @ 'french'
then we will get the result ‘Bonjour’.
What’s happening?
Perspective objects must understand a message forObject: o Selector: s
, where o is the current self of the object doing the lookup and s is the selector. They return a canonicalString. Strings know to return themselves, so ‘french’ is a shortcut for a perspective object which always returns ‘french’. The system finds the perspective object by sending a message to self, so
[o hello] @ french
will, as you would expect, look for the perspective object by sending the message ‘french’ to self.
Perspectives are placed on the process. They are not placed in a stack; but the @ message on traits block manages reinstalling the old perspective after itself, so if we have:
([o hello] @ 'french'), ' ', o hello
the result is ‘Bonjour Hello’
OK, so how is this done?
(1) We create the mixin:
mixins subjective = (|
ambiguousSelector: sel
Type: t
Delegatee: d
MethodHolder: m
Arguments: a = ( | l = lobby |
sel sendTo: self
DelegatingTo: ((l process this perspective forObject: self
Selector: sel) sendTo: self)
WithArguments: a
)
|)
This traps the vm send error message ambiguousSelector:Type:Delegatee:MethodHolder:Arguments:
and chooses which of the parents of our object is resent the message based on sending to the perspective object found on process this
the message forObject:Selector:
, which returns a string which is then sent to our original object to get the appropriate delagatee.
(2) globals process
is given a new assignable slot called ‘perspective’ with the contents the string ‘default
‘.
(3) traits block
is given a new slot @
@ p = ( | h. r |
h: process this perspective.
process this perspective: p.
r: onNonLocalReturn: [|:v| process this perspective: h. v ]
IfFail: [|:e| process this perspective: h. raiseError ].
process this perspective: h. r
)
which handles installing a new perspective and cleaning up after itself, and
(4) traits string
is given a new slot
forObject: o Selector: s = (self)
so that we can just use a string if we find it easier.
That’s it.
What can we do with it?
Well, first off let’s implement the error handling style mechanism from last post.
defaultBehavior = "Apart from all the other slots of course" (|
m* = mixins subjective.
default* = (| error: x = ( "Normal existing error code") |).
logErrors* = (| error: x = ( "Some code to log our error but not bring up debugger") |).
logErrorsPerspective = 'logErrors'
|)
Now we’re done. If we try to get
9 / 0
we get a debugger, whereas if we do
[ 9 / 0 ] @ logErrorsPerspective
then any error silently logged without a debugger being opened.
This isn’t perfect, and there are I’m sure lots of interestingly dangerous corner cases and unexpected behaviours lurking, but its a great example of way in which a simple but well thought out base like Self gives us great power. There are many languages where this wouldn’t nearly be as nice to play with.