Day 2 of Io takes a look at operators (which are really just methods with some syntactic sugar), and how you can add new operators to Io's operator table. It also introduces Io's capabilities for introspection, particularly message introspection.
We may have seen the slotSummary and slotNames methods that exists on Object that lets you inspect what slots exist on an object. Messages in Io are also capable of being inspected, and practically everything in Io is a message. While I have previously been referring to method calls, these are really messages being sent to an object that activates the method in that slot. Even when we are referring to an object "foo" that we may have created earlier. We are really sending a message to Object (global) to get whatever is in its "foo" slot. With this perspective we can look at some code.
Echo := Object clone
Echo who := method(
call sender type println # => caller
call message name println # => who
call target type println # => Echo
)
Echo who #=> Object, who, Echo
The who method, prints some information about the message passed in. TheĀ call message lets us inspect the currently activated slot (the method we are in). Sending call the sender message lets us determine who sent this message (i.e. from where the method was called), target is the receiver of the message, in this case the Echo object. 'Message' gets us the message itself, here we simply print the message name (who).
We can do more, such as get the arguments passed into a message
#all Io methods have variable arity
Echo howmany := method(
args := call message arguments
(args size asString .. " arguments passed in.") println
args foreach(arg,
arg println
)
)
Echo howmany("we", "can", "send", list(3,4,5,6,100), "many", "args")
Echo howmany("or", "justafew")
The message object returned from sending the message message to the object returned from call (phew! :)) contains quite a bit. Above we simply printed its name, it turns out it is actually a list of the rest of messages in the program. That is to say, if you do call message asString println you get all the code that follows the location in the file where this method was called from. Huh.
#messages povide access to the entire tree of messages for the program.
Echo context := method(
call message asSimpleString println
call message next next asSimpleString println
call message next next next asSimpleString println
)
Echo context
The calls to 'next' in the code above actually step through the message list, and its important to remember that these are not just strings, they are the actual messages! You can evaluate them, or even modify the list to change the program! Its quite a bit to wrap ones head about, and it immediately screams for abuse, but I haven't been particularly successful in this regard (though I haven't tried very hard), I've only been able to insert a single message that gets executed after which the program terminates (the rest of the tree got discarded). This yak remains unshaved (for now).
But on a serious note, one thing that makes all this possible is that messages are passed around unevaluated in Io. So for example when passing arguments to a method (note the arguments are a list of messages). The arguments are not evaluated first then passed in, rather they are passed in wholesale, so in most languages when you do
foo(4 + 3)
4 and 3 are added together to give 7 which is then passed into foo. In Io the message '4 + 3' is passed in. Now, Io methods do let you easily access the evaluated forms of the messages, which is why we haven't had to do anything particular strange when writing our methods. However the method introspection capabilities of Io let you get to the raw unevaluated messages. Mind = Blown!
This is what allows control structures like if and while to be implemented as simple methods. In fact Bruce Tate gives a 4 line implementation of an unless control construct (unless is like the opposite of if). Since methods are able to take unevaluated messages, you can passed in the code for the true and false conditions without them being evaluated at call time (before the condition is evaluated).
The following snippet show this.
Delay := Object clone
Delay send := method(
args := call message arguments
for(i,0, args size - 1, 2,
delay := doMessage(args at(i)) #do message turns the unevaluated '2' message into a '2' number
msg := args at(i+1)
System sleep(delay)
msg doInContext(call sender) #thought i'd be able to do 'call sender msg' but that does not work
)
)
Me := Object clone
Date asString("%H:%M:%S") println
#"Hello There! the time is " ..
Me time := method(at, "Hello There! the time is " .. at asString("%H:%M:%S") println)
Me delayMsgs := method(Delay send(1, time(Date now),
3, time(Date now),
5, time(Date now)))
Me delayMsgs
The delay object's send method, takes a message and a delay value, it then finds out who sent it the message and sends the message to that object after the specified delay. This particular code will also take any number of delay/message pairs. If you run this, you see the program print the current time, wait a second, print the time, wait three seconds, print the current time and wait 5 more seconds and print the current time. In languages that evaluate arguments before passing them in. The printed time would be the same in all cases as 'Date now' would be evaluated at the call time of delayMsgs.
The code also shows how to evaluate arguments in the context of the curent message (doMessage), and have a message be evaluated in another context/object (the doInContext method).
Day 2 was fun! And day 3 promises not to disappoint, DSLs and Concurrency are the topics for discussion.
Self Study Exercises
The self study exercises weren't really about message introspection. My answers are available on github here. The questions involve: modifying existing operators (changing how divide works), creating types/protos (like classes) to do matrix operations, a little bit of file i/o and console i/o.