Week 2 of the Seven Languages in Seven Weeks book is about the Io Language. Day 3 is more specifically on how the flow of control (how messages are passed to an object or to its parent) can be hijacked to create a DSL.
In this post, I am going to describe an interesting feature in Io, and how I extended it a bit to enhance an XML generator.
In the example provided, the following Io code:
Builder ul( li("Io"), li("Lua"), li("JavaScript"))
can generate the following output:
<ul> <li> Io </li> <li> Lua </li> <li> JavaScript </li> </ul>
This is made possible by redefining the forward method (“slot” in the terminology of Io), which is responsible for deciding who to apply the method calls (“message calls” in Io) to.
The other key thing is that it is possible to obtain the details on a message call, even when this message is not defined, a bit like what method_missing does in Ruby. This is done using call message.
For example, while this would be a normal method definition:
Builder myMethod := method("I have been called" println)
you could also do it awkwardly by redefining forward and using call message:
Builder forward := method(if(call message name == "myMethod", "I have been called" println))
This leads us to the following program (provided in the book) as a way to implement the Builder called in the example:
Builder := Object clone Builder forward := method( writeln("<", call message name, ">") call message arguments foreach( arg, content := self doMessage(arg); if(content type == "Sequence", writeln(content) ) ) writeln("") )
The exercise was to add indentation dependent on the depth of the tag. It turns out that slots (that is, members of classes or instances) can added to pretty much anything in Io, so I decided to add a depth slot to arguments. Here is the resulting Io code:
// create a sub-class ("prototype") of Object Builder := Object clone // redefine the forward method in order to intercept unknown method ("message") calls Builder forward := method( // if the message does not have a "depth" attribute ("slot"), add it call message hasLocalSlot("depth") ifFalse(call message depth := 0) // indentation is "depth"-times a tab indent := "\t" repeated(call message depth) // open an XML tag, the name being the name of the message, // preceded by the indentation writeln(indent, "<", call message name, ">") // loop on each of the arguments of the message call message arguments foreach( arg, // increase the depth level for the sub-message call arg depth := call message depth + 1; // call the message represented by the argument on the current instance // (or on the current prototype, as it is the case here) content := self doMessage(arg); // check if the argument turns out to be a normal String if(content type == "Sequence", // prints the value of the argument, preceded by the indentation writeln(indent, content) ) ) // closes the XML tag writeln(indent, "") )
With this, the folllowing code
Builder p ( ul( li("Io"), li("Lua"), li("JavaScript") ) )
produces this result:
<p> <ul> <li> Io </li> <li> Lua </li> <li> JavaScript </li> </ul> </p>
To sum up, we have created a DSL by redefining the way methods are called, introspecting the method call, and finally added a parameter to the method call (!).
If you want to read more about this technique, check out this other post on Io in the 7 Languages book. Curiously, Ben Nadel uses a variable on the Builder to store the current depth. That works, but it seems to me that it behaves just like a global variable, which wouldn’t be thread-safe. Also, I rather like associating a tag to its corresponding depth.