Saturday, January 05, 2008

Reflection on Everything

Smalltalk allows you to reflect on everything. You can not reflect on objects to find their instance variables and methods. But, you can also reflect on code running and on the stack. It's powerful stuff. Why would you need this extra power? You might be a lazy developer. You might constantly remind yourself, "I work in a dynamic language and it should work for me!" Let's go digging shall we?

One thing you have to be careful in dynamic languages are message not understood or method missing errors. One easy mistake to make is to do the following:
[someObject doSomething] 
on: MessageNotUnderstood
do: [:ex |
"log something"
^self]

What's wrong with the above code? Well, are you trying to catch if doSomething is not understood by someObject? If so, the call can still succeed and there could be a nasty bug further down in the code. The handler will be giving misinformation on the true problem. Frustrating to say the least. It's better to do something like this:
[someObject doSomething]
on: MessageNotUnderstood
do: [:ex |
(ex message selector == #doSomething and: [ex receiver == someObject])
ifTrue: ["log"
^self]
ifFalse: [ex pass]]

Yuck. But, it does check the receiver and selector to make sure we captured the right exception. Lots of typing for something simple. Granted you shouldn't be doing a lot of guarding against MessageNotUnderstoods (polymorphism anyone?). But, sometimes it is necessary. Besides, we wouldn't have this fun little blog post would we. Basically, the above method checks for the right selector and receiver that we expected to have a MessageNotUnderstood and if it did we do our logging code. If not, it's something we didn't account for and is a bug, thus we "pass" the exception to the next handler. But, how can we prevent ourselves from all of this typing?

Squeak has this method implemented:
BlockConext>>onDNU: selector do: handleBlock
"Catch MessageNotUnderstood exceptions but only those of the given selector (DNU stands for doesNotUnderstand:)"

^ self on: MessageNotUnderstood do: [:exception |
exception message selector = selector
ifTrue: [handleBlock valueWithPossibleArgs: {exception}]
ifFalse: [exception pass]
]

It does not check the receiver, but that's OK. It turns our code into this:
[someObject doSomething] onDNU: #doSomething do: ["log something" ^self]

Nice. It's much more succinct, but I don't like the duplication of "doSomething". What are we to do? I came up with this method:
BlockConext>>onImmediateNotUnderstoodDo: anExceptionBlock 
^ self
on: MessageNotUnderstood
do: [:problem |
| myContext problemContext |
myContext := thisContext home.
problemContext := problem signalerContext sender sender sender.
(problemContext == myContext
and: [self method messages includes: problem message selector])
ifTrue: [anExceptionBlock
valueWithPossibleArgs: (Array with: problem)]
ifFalse: [problem pass]]

Here's what it makes our code look like now:
[anObject doSomething] onImmediateNotUnderstoodDo: ["log something" ^self]

It looks like what we started with, but this version is safe. It will pass the MessageNotUnderstood exception if the send did not happen directly from the block we defined.

What is the method above doing? The method I wrote reflects on the stack and the compiled code. I use the stack to find the receiver that should be in the block. The compiled code is needed to find all of the selectors that are called directly from the block. Pretty cool, huh?

Smalltalk is one of the few languages where you can reflect on everything on the stack. Stack frames are objects too. It makes doing difficult things possible.

1 comment:

Andres said...

Even more powerful:

[:ex | ex occurredInTheContextOfSomethingInteresting]

Then, let each exception do as it pleases based on polymorphism.

Thanks,
Andres.

Amazon