Pharo Superpower: Respond to any Message
Tuesday, January 19th, 2010
For the fifth week in a row we’re stepping into the Pharo superpowers booth. Today we shall learn how to create objects that respond to any message. That is, objects that respond to a message without implementing a corresponding method. Again, as with sending any message, this superpower can be used for the good (if used with care) and I will thus discuss an example that I consider good use below.
When a message is sent to a Smalltalk object, the message name is looked up in the method dictionary of the object’s class and its superclasses. If a method whose name matches the message is found, that method is executed. However, if no matching method is found a special message is sent to the object, which is
#doesNotUnderstand:
By default, the implementation of #doesNotUnderstand: opens a debugger (or more precise, the pre-debugger dialog that we all know from test-driven development). However, we are free to override #doesNotUnderstand: and thus respond to any unknown message.
As a (dadaistic) example, let’s implement a Lorem ipsum object
Object subclass: #Lorem instanceVariableNames: 'expects'
with an ipsum constructor
Lorem class >> ispum
^ self new
and the following two methods
Lorem >> initialize
expects := #(dolor sit amet "to be continued ad nauseam..." nil)
Lorem >> doesNotUnderstand: aMessage
^ aMessage selector == expects first
ifTrue: [ expects := expects allButFirst. self ]
ifFalse: [ super doesNotUnderstand: aMessage ]
So, if you ever doubted that virtually any English sentence is valid Smalltalk, here is your proof :)
Lorem ipsum dolor sit amet.
This executes as valid Smalltalk code, without ever having implement any #dolor , #sit or #amet method! If however, we deviate from the canonical Lorem ipsum sequence we’ll get the usual MessageNotUnderstood error.
[ Lorem ipsum dolor zork ] should signal: MessageNotUnderstood.
As a more sensible examples let’s consider a list that responds to any messages understood by all its elements.
OrderedCollection subclass: #Group.
Group >> eachRespondsTo: aSelector
^ self allSatisfy: [ :each | each respondsTo: aSelector ]
Group >> doesNotUnderstand: aMessage
^ (self eachRespondsTo: aMessage selector)
ifTrue: [ self collect: [ :each | aMessage sendTo: each ] ]
ifFalse: [ super doesNotUnderstand: aMessage ]
As you can see, the implementation of #doesNotUnderstand: follows the same pattern as above. We check whether we want to handle the message, and if not, we delegate to the default implementation in object (which will open a pre-debugger dialog).
Keen readers might have already noted a limitation of above approach: when you override #doesNotUnderstand: but not #respondsTo: your object will respond to a new message (through the means of #doesNotUnderstand:) but still insists that it does not respond to that message when queried with #respondsTo:.
So we’ll have to override #respondsTo: as well
Group >> respondsTo: aSelector
^ (super respondsTo: aSelector) or: [ self eachRespondsTo: aSelector ]
It is a sad but true fact, that over 90% of all #doesNotUnderstand: overriders that you’ll find out there do not override #respondsTo: as well—even though they should!
So now our new class is ready for a bunch of expectations (please refer to Phexample for more details on expectation matchers)
g := Group new.
g should not respondTo: #x.
[ g x ] should raise: MessageNotUnderstood.
g add: 2 @ 3.
g add: 3 @ 4.
g add: 1 @ 2.
g should respondTo: #x.
g x should beSameSequence: #(2 3 1).
g y should beSameSequence: #(3 4 2).
BTW, if you are an OSX user and looking for a language that provides this feature by default, take a look at F-Script by Philippe Mougin. F-Script also offers a plethora of awesome features beyond projection of messages, for example it allows you to manipulate the Cocoa objects of any OSX application—at runtime!
As a best practice, you should only override #doesNotUnderstand: and #respondsTo: on your own classes. Just imagine what might happen when two or more stakeholders attempt to override #doesNotUnderstand: in, for example, Collection, only one of the extensions will eventually remain and thus leave the system in an undefined state with overloaded extensions.
If you know more good (or evil, uoarharhar) uses of #doesNotUnderstand: share them in the comments.
Hackety hacking!
