Today we will continue the series of articles
begun by the honorable semka. Let's talk about messages.
There are no function calls in Io, but there is a message sending. A message may have arguments (almost like function arguments), but message arguments are not executed before being sent.
How messages are sent
Sending a message looks like “object”, “space”, “message”:
Database connect
The message may contain arguments:
Database findByName("Oleg")
The result of sending a message can also be sent a message:
Database findByName("Oleg") lastName # , "Andreev"
In other words, the
abcd
construction is equivalent to
a().b().c().d()
in some java-like syntax.
')
How messages are performed
First, remember that message arguments are not executed. If
Database connect(blah-blah)
was written, then blah-blah will not be executed before sending connect, but will be transmitted as part of the "connect (blah-blah)" message (yes, in words).
When an object receives a message, it searches for a slot with the name of this message (in our example
connect
). If the slot is not found (as it usually happens), its search is performed recursively in all object prototypes and their prototypes. If the required slot is not found anywhere, then the search for the slot
forward
started (analogous to
method_missing
in Ruby). As soon as any suitable slot is found, its value is
activated (activation). (Note: the object in which the found slot lies in will tell you the Object contextWithSlot (slotName).)
Activation
For normal values, activation does nothing, just returns that value. Thus, the activation of regular slots that store numbers, strings, or many other objects is no different from getting a value via getSlot (
slotName ). But those objects that are marked as
activatable , call the
activate method. By default, only two objects are activated: Block (blocks and methods) and CFunction (links to syshny functions). All other objects can be made activated with setIsActivatable (true).
Slot activation does not occur when the getSlot (slotName) method is called (of course, the “getSlot” slot itself is activated, but the
slotName is no longer). Therefore, if you want to get a method or an as-is block without calling it, use getSlot (methodName).
With activation is associated with one pitfall, which will be discussed at the end of the article.
Uh And when are the arguments executed?
Message sent, slot found and activated. But when and in what context will the message arguments be calculated?
Consider an example:
withoutArgs := method() # nil
withoutArgs("Hello!" println)
This code will not output anything because the argument "Hello!" Println was not executed.
withArgs := method(a, b, c, list(a,b,c)) #
withArgs("Hello!" println) # list("Hello!", nil, nil)
This code will execute the first argument and display Hello !. Missing arguments will be nil.
One more example:
withTwoArgs := method(a, b, nil)
withTwoArgs(1 print, 2 print, 3 print, 4 print)
This code will print 12, but not 1234.
The most shrewd already understood that only the declared arguments are executed. The funny thing is how they are executed, where and where you can run your dirty hands. And why all this is necessary.
Introspection method call
Do you still remember that in Io there are only objects, prototypes, slots and messages? There are no global and local variables in this list.
When Io activates a slot (that is, calls a method), it creates a special Locals object. This is a pretty fun object with a number of important features. First, the prototype of this object is self, i.e. pointer to the object to which the message was sent. Thus, we can create local slots (aka local variables) without interfering with the recipient object, as well as get access to all slots of this object. Secondly, there are at least three slots in this object: self (points to the recipient object), call (contains a lot of information about the call), and updateSlot. The latter is different from the usual updateSlot in that it updates the slot not in the locals, but in the receiving object. This is done solely for the sake of convenience, to write a = b instead of self a = b.
The most remarkable thing about the call object is a few slots:
- call sender - the object (locals) in which the message was sent.
call target - the object to which the message was sent (== self).
A call message is actually a message containing a name and arguments.
call evalArgAt (argNumber) and evalArgs executes some or all of the arguments in the context of a call sender. But no one bothers to do something like this:
Object do := method(call message arguments first doInContext(self) )
"string" do(
type println
size println
encoding println
)
On the screen we will see:
Sequence
6
ascii
In other words, we can manipulate the code as we like: transfer, store, execute in arbitrary contexts. You can see the code of the object already created: Coroutine getSlot ("yield") code will return the yield method code as a string:
method(
if(yieldingCoros isEmpty, return )
yieldingCoros append(self)
next := yieldingCoros removeFirst
if(next == self, return )
if(next, next resume)
)
By the way, about the prototype locals I lied. If you still remember, besides the methods in Io there are blocks (closures). Their only difference from the methods is that the Locals prototype for blocks is not a pointer to the recipient of the message, but a pointer to scope, i.e. the object in which the block was created (and to which it is closed). So that you finally leave the roof, I will add that the slot of the scope block is exactly equal to the call sender at the time of calling the block method.
Back to local variables
Possessing such a powerful weapon as call sender and call message, we can describe the method call and the execution of arguments in Io language. Indeed, by saying method (a, b, nil), we inform that we want to create slots “a” and “b” in locals and fill them with values ​​that are obtained after executing the corresponding arguments in the context of a call sender. Homework: write the method method2, which creates the methods as well as the built-in method and performs all the necessary manipulations with doInContext for the declared and passed arguments.
Promised trick with activation
In java-like syntax, access to a function and its call are as follows: obj.func and obj.func (). In Io, retrieving a value without activation is possible via obj getSlot ("slot"), when an obj slot necessarily activates a slot value.
Suppose you are writing a method that can take another method as an argument:
method(func, ...)
If you write func inside the body of a method, you will inevitably call the passed method. If you just need to get its value, then you will have to access it every time via getSlot (“func”).
Constructing messages
The message method returns its first argument as a failed message:
msg := message(something(arg1, arg2, arg3) nextMessage(arg1, arg2) more)
msg name # => "something"
msg next # => message(nextMessage(arg1, arg2) more)
msg next next # => message(more)
msg next name # => "nextMessage"
msg arguments # => list(message(arg1), message(arg2), message(arg3))
Summary
Now you know how messages are sent, methods are called, and arguments are executed. I hope you understand the constructions like list (1,2,3), foreach (print) (print is sent to each element of the list) or list (1,2,3) map (* 2) (* (2) is sent to each element when creating copies of the list).
Bonus track: call sites
One and a half diggers who read to the end of the article can be rewarded with particularly delightful information. In the concept of Io, it is possible to reach the so-called "call point". This is not a trickle, not the context of aka "call sender", but the point of the code at which the call occurred. The thing is that the call message always returns the same object if it is not generated on the fly, but is described in the code (well, or generated once and all the time is sent). What does this give? Since one method can process various messages (in different places of the program), it becomes possible to cache something useful in relevant places.
The simplest thing is: Message setCachedResult (res) sets the computed message value to be returned when trying to send this message to somewhere. A more complicated option: cache a special logic adapted for a specific message at a given call point.
For example, the List map method has three implementations: with one, two, and three arguments. In the current implementation, the check of the number of arguments occurs on each call, although it is possible to cache the desired method variant in a call message.
Based on this functionality, it is possible to build more complex optimization schemes.