This video will focus on an advanced aspect of Pharo,
which is the reflective operations needed for the live programming we use.
We will look at what happens when we recompile a class.
And we'll explain the reflective operations
needed for implementing live programming.
Here's a typical case.
We usually start by defining a class,
then we add methods and create instances for this class.
Then we redefine the class and add new instance variables.
So, what do we do with the instances that already existed
when there was one less attribute?
We must set up a mechanism to migrate these instances
over to the new version of the class.
Then we keep working.
After that, we will look at operations
that enable dynamic class redefinition, method recompilation,
and which allow for
an automatic and transparent migration of instances
to the new version of the class.
To do this, we need to collect all the class instances.
Then we need to switch over everything that used these objects
and assign their pointers to new objects.
Okay?
First, how do we collect all the instances of a class?
If we send the allInstances message to a class,
it shows all the objects that are its instances.
It's a collection of all the standard objects.
If we send it to Window class, we get a collection of Window objects.
We can retrieve the first element and send a "close" message,
since the "close" method is defined in the Window class.
Another reflective primitive is pointersTo.
If we send this message to an object, we access the collection
of all objects that store a reference to the object.
This is very useful
when we want to swap objects.
The primitive for swapping pointers is "become."
The idea is to enable us to exchange two objects.
We want to swap them symmetrically.
Everything that pointed to this object in the system,
in this case we have two pointers,
will be inverted so that the elements now point to this object.
We break the links for the pointers that were on this side,
so they switch objects.
"Become" means to invert all pointers in the system symmetrically.
Here's an example.
I create a point 1,
which points to the object point 0@0 and a variable pt2,
which also points to the object 0@0.
Plus a variable pt3 that points to the point 100@100.
Then I write the primitive pt1 become: pt3.
Everything that pointed to the object denoted by the variable pt1
will now point to point 3.
That is, all the pointers to pt1.
We see that the variable pt2 pointed to the same object as pt1,
and now it points to that which was referenced by pt3.
As for pt3, since it's symmetrical, it will point to what pt1 pointed to.
Up the top.
And pt1 points to what pt3 pointed to.
The pointers have thus been swapped symmetrically.
The asymmetrical variant is becomeForward.
It indicates that we want to swap pointers.
Everything that pointed to this object will swap over to that object.
But not the contrary. Pointers to this object stay the same.
Here's another example using points.
When we execute a becomeForward,
we see that it impacts on pt1 and pt2.
Pt3 was not affected by the becomeForward,
and those that pointed to this object are unchanged.
Another reflective primitive is adoptInstance.
This changes an object's class.
We ask a class to adopt a new instance, passed as a parameter.
Changing a class is a very powerful, but rather limited, tool.
It's essential that the object's original class
in this case, anInstance,
is compatible with the format of the target class.
You can't just swap any object.
Sometimes it's indexed, etcetera.
The format of an object is very important.
Let's look at what a class is in essence.
A class is essentially a format.
A format specifies the number of instance variables
and the type of variables, as we explained in an earlier course.
It has a superclass and a method dictionary.
Here we have Behavior class, which is a superclass
of Class class, which defines basic class behavior.
It's the basic minimum for a class.
So, a class has a superclass, a method dictionary,
and format specifications.
I'll now summarize all the reflective behavior features
that we've discussed so far,
which give certain instances behavior that is specific to them.
I'll explain the code as we go along.
We'll create an instance of Behavior class on these three lines.
Remember, it's a class.
I create an instance of the class, which I name Behavior.
That gives me an object named Behavior.
I indicate that this object inherits from Model class.
Like this.
I set the format of this object Behavior.
Then I create an instance of Model class here.
It's an object named Model.
Here is the important line in this code.
I'll change the class of this object Model
to become a class of the object passed as a parameter, named Behavior.
I break the link and make it an instance of this class.
This is a subclass of Model class.
Now I can compile a new method in this class.
I compile with the method foo, which returns 2.
I'm doing this in the object Behavior.
As we can see here,
if I send the message foo to the object Model over here,
like this,
it will return 2.
This follows the lookup method.
I send the foo message.
The object Model searches for the method in its class, the object Behavior,
and successfully finds the method.
It works perfectly.
But if I create a new instance of Model class,
like this,
and I send the message foo,
an error is indicated by MessageNotUnderstood.
Because if I apply ToLookUp,
I look in the object's class, Model class,
but I don't find the foo method in its dictionary or its superclasses.
This advanced mechanism allows us to endow a specific instance
of Model class with a behavior.
Let's apply this to Set class to make it clearer.
I create set1, an instance of Set class.
The Set class method dictionary contains the method add:.
For a specific instance of Set class, I want to change the method add:,
which has a specific behavior for a specific set.
So, I create an "anonymous" class here.
It's a specific class that inherits from Set class.
Here is its inheritance.
The object set2 is an instance of this "anonymous" class.
In its method dictionary, I'll redefine the method add:
by giving it a particular behavior.
Now, if I send the message add: to the object set2,
this method will be selected,
and it will have a specific, different behavior
from the method add: here if we had sent the message to this set.
So, we have two sets that react differently to the message add:.
Here is the code to conduct this exercise.
Just like before, I create a class
as an instance of Behavior class.
I set the superclass at Set class.
I set the format.
I compile the method add: in this anonymous class.
I'm redefining the method add: that is defined in Set class.
I use "Transcript show" to display the execution of the method add:.
Then I call "super" to execute the method add: in Set class.
Now we'll test this code by creating a first set.
I have Set class here.
I create the first instance of this class, named Set.
I send it the message add:.
I send add: 1, which uses the method add: from Set class.
If I ask for Set class here, I'll get Set.
But now I'll execute this primitive you see here.
I ask the object Set to change classes
and to use Behavior class from earlier, which is a subclass
of Set class.
So, it changes and becomes an instance of Set class,
which has a new version of the method add:.
Now, if I send the message add: 2 to this object,
it will select the method add: from the object Behavior,
and it will execute this code.
We can check in the Transcript
if the method has been executed.
It will eventually be executed,
since we also use "super" for the method add: from Set class.
This allows us to spy on instances selected in Set,
using this line here.
To conclude,
all these reflective operations allow us to create very innovative tools.
We can test out and implement new features
thanks to these reflective primitives, which promote language extensibility.
But there's a golden rule when using reflection.
You must not use it inappropriately in domain code.
We limit the use of reflective codes in domain code.
Reflection can violate the encapsulation of objects.
It can really...
It can overstep object programming rules,
and is therefore reserved for constructing tools.