I haven’t discussed or explained much about the metaprogramming capabilities of Essence#. So let me do that now. First, I will present some example code, and then I will explain what it does, and how it does it:
| behavior array | behavior := Behavior new. behavior superclass: #() class; uses: Examples.Debutant; addMethod: [## whatAreYou self class superclass name showCr ]. array := #(5 8 13). array changeClassToThatOf: behavior new. array introduceYourself; whatAreYou. array detect: [:each | each \\ 2 = 0]
What the example code does
The example code shows how to define instance-specific behavior for an object.
In this specific example, the array #(5 8 13)–which is an array of the three integers 5, 8 and 13–is made to also have the behavior defined by the trait Examples.TDebutant, and it is also bound to a method defined dynamically by the code of the example–one that no other array instance in the whole universe will have.
Because of the new behavior added uniquely to this one particular array object, it (and it alone) will understand the messages #introduceYourself (imported from the trait Examples.TDebutant) and #whatAreYou (dynamically defined and bound solely to the one array instance by the code of the example.)
So when the array instance #(5 8 13) is sent the message #introduceYourself, it responds by writing the text ‘Hello, world! I”m a Debutant!’ to the Console. And when it is sent the message #whatAreYou, it responds by writing the name of the superclass of its class to the Console. However, were either of those messages sent to any other array objects, the result would be a MessageNotUnderstood exception (that’s true even if any of the other arrays also contained the same three elements–only the object identity of a message receiver matters when one engages in this sort of metaprogramming.)
The point of the final statement of the example is to demonstrate that the array instance whose behavior was modified by the code of the example nevertheless continues to behave like a normal array object in all other respects. The final statement evaluates to the first element of the array that, when divided by 2, has a remainder (technically, a residual) of zero. So it evaluates to 8, which is the same result that one would obtain by sending the same message to any other array instance whose first even-numbered element was the integer 8 (assuming all of its elements were numbers.)
How the example code works
The code in the example does its magic by creating a new class (technically, a new Behavior, which you can think of as a “lightweight class” or “proto-class”), sending the new class the message #uses: Examples.TDebutant so that will import the methods defined by the trait Examples.TDebutant, sending the new class the message #addMethod: with a method literal object as the message argument, and sending the new class the message #superclass: with the class of an empty array object as the message argument (which will, of course, be the same class as that of the array #(5 8 13)). But all of that is only the appetizer; by itself, it would not be sufficient to modify the behavior of the array object #(5 8 13).
The master spell that takes us to the next level is performed by the magic message #changeClassToThatOf:, whose receiver can be any Essence# object whose object state architecture is compatible with the instance architecture of the class of the object that is the argument to the message. As you might infer from the name of this spell…uh, message, the effect of uttering (sending) it is to change the class of the receiver. In this case, it changes the class of the array object #(5 8 13) to be the new class we’ve created.
Obviously, the reason the array object #(8 5 13) acquires new behavior in the example code is because its class gets changed. And the reason it–and only it out of all other array objects–gets the new behavior is because we only changed the class of the one array object, and not the class of any other array. And the reason it gets the ability to respond to the messages #introduceYourself and #whatAreYou is because we specifically added methods with those names to the array object’s new class (note that the order in which we add the new behavior to the class and change the class of the array doesn’t matter, provided both of those changes are accomplished before we try to make use of them.)
And finally, the reason the array object continues to have array behavior after we’ve changed its class is because we made the array’s original class be the superclass of the new class we created. Since the array’s original class is the superclass of its new class, any messages sent to it will still end up being handled by the methods of its original class.