Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with:

Seven Languages In Seven Weeks: Io - Day 3

Posted by Ben Nadel
Tags: Io

I just finished day 3 of the Io language in my Seven Langauges in Seven Weeks book. I don't have a lot of time to write today, but like day 2, this homework was again both challenging and fun. The fact that you can override just about everything in the Io languages makes for a powerful set of features that can be leveraged to accomplish some pretty amazing stuff. Time and time again, I'm finding that two of the coolest features of the Io language are the fact that messages have delayed evaluation and that you can arbitrarily execute messages in the context of any object. This makes for a highly dynamic language.

HW1: Enhance the XML program to add spaces to show the indentation structure.

  • // Enhance the XML program to add spaces to show indentation
  • // structure.
  •  
  •  
  • // Start off our XML builder by cloning the base Object.
  • XMLBuilder := Object clone();
  •  
  •  
  • // I am the depth of the current branch of the XML tree. In order
  • // to output the XML in a meaningful way, we will need to keep track
  • // of the depth at each level of recursion. This will define a
  • // getter/setter for this property.
  • XMLBuilder parseDepth ::= 0;
  •  
  •  
  • // I override the core "forward" method which inherently passes
  • // unhandled messages up to the prototype object for handling
  • // (and so on up the prototype chain). We can use this to handle
  • // methods dynamically without defining them.
  • //
  • // NOTE: Be very careful with this kind of function because it can
  • // quickly become a recursive nightmare.
  • XMLBuilder forward := method(
  • // Get the name of the method that was being invoked in this
  • // "failed" message.
  • missingMethodName := call message() name();
  •  
  • // Get the white space that we want to output before the
  • // XML contstructs at this level. Each depth of the XML
  • // tree will be represented by four dots.
  • //
  • // NOTE: We are using dots rather than spaces since my blog
  • // will not be able to handle multiple spaces (HTML).
  • prettyPrefix := ("...." repeated( self parseDepth() ));
  •  
  • // Output the opening tag for this XML node.
  • writeln( prettyPrefix, "<", missingMethodName, ">" );
  •  
  • // Now that we have output the starting tag, we are going to
  • // parsing at a bigger depth. Increase the depth by one.
  • self setParseDepth( self parseDepth + 1 );
  •  
  •  
  • // Now that we have output the tag, we need to recursively
  • // invoke the arguments. Loop over each argument.
  • call message() arguments() foreach( arg,
  •  
  • // Execute the arguments in the context of this XML Builder
  • // instance (self). In doing so, we will be able to catch
  • // further messages with this forward() method.
  • content := self doMessage( arg );
  •  
  • // Check to see if the result of the invocation was a string.
  • // If so, we are going to output it. However, when doing so,
  • // we *will* be at a depth lower. As such, add more dots to
  • // the pretty prefix.
  • if(
  • (content type() == "Sequence"),
  • writeln( prettyPrefix, "....", content );
  • );
  •  
  • );
  •  
  •  
  • // Now that we have handled all of the nested children, we are
  • // going to be coming back up one level in the XML tree.
  • // Decrement the parse depth.
  • self setParseDepth( self parseDepth() - 1 );
  •  
  • // Output the closing tag for this XML node.
  • writeln( prettyPrefix, "</", missingMethodName, ">" );
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Build our XML output using our new DSL (domain specific language).
  • XMLBuilder ul(
  • li( "Io" ),
  • li( "Lua" ),
  • li( "Javascript" )
  • );

In the book, all of the XML constructs were output on their own line, flushed left. In order to provide structure for the output, the depth of the parsing needed to be known. Since each node is being parsed independently of every other node, I kept track of the depth outside of the node evaluation. Then, I was able to add a white space prefix to each line based on the depth of the current node. And, since parsing of the XML tree takes place in a depth-first, incrementing and decrementing the depth during the parsing process was fairly straightforward.

When we run the above code, we get the following console output:

<ul>
....<li>
........Io
....</li>
....<li>
........Lua
....</li>
....<li>
........Javascript
....</li>
</ul>

To see a much more robust XML example, look at HW3.

HW2: Create a list syntax that uses brackets.

  • // Create a list syntax that uses brackets.
  •  
  •  
  • // Override the method that gets called when matching curly brackets
  • // are encountered within the code.
  • curlyBrackets := method(
  • // We are going to map the arguemnts of the brackets into a new
  • // list which we will return.
  • mappedList := call message() arguments() map( value,
  •  
  • // Execute the message in the current context and return the
  • // result as the mapped list item.
  • self doMessage( value );
  •  
  • );
  •  
  • // Return the mapped list.
  • return( mappedList );
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Create a list of women using bracket-notation.
  • women := {
  • "Sarah",
  • "Katie",
  • "Tricia",
  • "Jill, Jr."
  • };
  •  
  • // Print the size of the list.
  • ("Size: " .. women size()) println();
  •  
  • // Print the list.
  • women println();
  •  
  • // Print last item (to show nested comma).
  • women at( women size() - 1 ) println();

In the Io language, the use of curly brackets (or curly braces) causes the curlyBrackets() method to be invoke in the context of the current object. In order to use the curly brackets as a syntactic construct, we simply need to define a curlyBrackets() method in the current context and then use the arguments that get passed to it.

Since message evaluation in the Io language is delayed, we have to manually execute each argument as a message on the current context. That's why we are calling doMessage() on each curlyBrackets() argument. This will give us the value of the argument which we can then append to the given list.

When we run the above code, we get the following console output:

Size: 4
list(Sarah, Katie, Tricia, Jill, Jr.)
Jill, Jr.

Notice that even though our fourth argument ("Jill, Jr.") had a nested comma, Io was able to understand it as a single, quoted value.

HW3: Enhance the XML program to handle attributes: if the first argument is a map (use the curly brackets syntax), add attributes to the XML program.

For this problem, I wanted to take it up a notch. Not just for fun but, because I felt that adding a little complexity upfront would actually make our lives easier in the long run... OK, and because it was more fun. In this problem, rather than simply augmenting the XML example, I wanted to come at the situation in a more Object Oriented mannor. Rather than just outputting XML, I wanted to parse it into an XML document model and then output the XML by printing the XML document.

To do help accomplish this, I need to create a few classes:

XmlDocument - This object contains a single root XML node. We are using this object to help print the document by overriding the asString() method. It will then recursively print the XML markup.

XmlElement - This is our XML node model. It has a name, text, parentNode, and childNodes collection.

XmlAttributes - This is a specialized Map that allows us to add only simple values (Numbers and Sequences) as the value for our attributes. It does this by overriding the atPut() Map method, passing messages up to the super() object only when necessary.

XmlParser - This is the object that actually parses the XML IoML (Io Markup Language). It handles all of the missing-method functionality, curlyBrackets(), and ":" assignment operator that allow us to treat our messages as a domain specific language (DSL).

  • // Enhance the XML program to handle attributes: if the first
  • // argument is a Map (use the curley brace syntax), add attributes
  • // to the XML program.
  • //
  • // Example: book( {"author":"Tate"} ... )
  •  
  •  
  • // First, we need to define a new assigment operator. This will cause
  • // the given operator to be parsed as a call to the given method on
  • // the given context.
  • OperatorTable addAssignOperator( ":", "atPutPair" );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // I am the XML document. I only contain an XML root element.
  • XmlDocument := Object clone();
  •  
  •  
  • // I am the XML document constructor. I get called when ever clone()
  • // is invoked.
  • XmlDocument init := method(
  •  
  • // I am the root node.
  • self rootNode ::= nil;
  •  
  • // Return this object reference for method chaining.
  • return( self );
  •  
  • );
  •  
  •  
  • // I print the XML document as a pretty string with indentation.
  • XmlDocument asString := method(
  •  
  • // Return the printed version of the entire XML document.
  • return(
  • self printNode( rootNode(), 0 )
  • );
  •  
  • );
  •  
  •  
  • // I get the pretty prefix we are going to use for outputting the
  • // XML document at a given depth.
  • XmlDocument getDepthPrefix := method( depth,
  •  
  • // Each depth will be represented by four dots since my blog
  • // cannot handle multiple spaced :)
  • return(
  • "...." repeated( depth )
  • );
  •  
  • );
  •  
  •  
  • // I print the given XML node at the given depth.
  • XmlDocument printNode := method( xmlElement, depth,
  •  
  • // Create a string buffer for our output.
  • buffer := list();
  •  
  • // Friendly version of new line.
  • newLine := "\n";
  •  
  •  
  • // Get the prefix for out output.
  • depthPrefix := self getDepthPrefix( depth );
  •  
  • // Output the start tag. This is a bit complicated because we
  • // are adding attributes here as part of the representation of
  • // the open tag.
  • buffer append(
  • depthPrefix,
  • "<",
  • xmlElement name(),
  • if(
  • (xmlElement attributes() size() > 0),
  • (
  • // Create a buffer for our attribute list.
  • attributeBuffer := list( "" );
  •  
  • // Convert each name-value pair to quoted attribute
  • // notation name="value".
  • xmlElement attributes() foreach( name, value,
  • attributeBuffer append(
  • name .. "=\"" .. value .. "\""
  • );
  • )
  •  
  • // Flatten the attribute buffer.
  • attributeBuffer join( " " )
  • ),
  • (
  • // No attributes, just return empty string.
  • ""
  • )
  • ),
  • ">",
  • newLine
  • );
  •  
  • // Output any text value for this element. Notice that the text
  • // is at a deeper depth than the actual node.
  • if(
  • xmlElement text(),
  • buffer append(
  • self getDepthPrefix( depth + 1 ),
  • xmlElement text(),
  • newLine
  • );
  • )
  •  
  • // Output each child node.
  • xmlElement childNodes() foreach( childNode,
  •  
  • // Print the child node.
  • buffer append(
  • self printNode( childNode, (depth + 1) )
  • );
  •  
  • );
  •  
  • // Output the close tag.
  • buffer append(
  • depthPrefix,
  • "</",
  • xmlElement name(),
  • ">",
  • newLine
  • );
  •  
  • // Return the element output buffer.
  • return(
  • buffer join( "" )
  • );
  •  
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // I am the XML Attributes object. Each XML node will have an
  • // attributes collection. This set of name-value pairs can be
  • // defined using the { name:value } notation within the IoML.
  • XmlAttributes := Map clone();
  •  
  •  
  • // I proxy the put() method to make sure that we are only adding
  • // simple values as the attirbute value.
  • XmlAttributes atPut := method( name, value,
  •  
  • // For XML attributes, we only want to allow our values to be
  • // simple. That is, sequences and numbers.
  • if(
  • list( "Sequence", "Number" ) contains( value type() ),
  •  
  • // Add the name-value pair to the attribute collection.
  • // Since we have overriden the atPut() method, we need to
  • // pass this message up our prototype - Map.
  • super( atPut( name, value ) );
  • );
  •  
  • // Return this object reference for method chaining.
  • return( self );
  •  
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // I am the XML Element object. I contain a set of attributes and a
  • // set of child nodes.
  • XmlElement := Object clone();
  •  
  •  
  • // I am the Xml Element constructor. I get called with no arguments
  • // whenever the clone() method is called.
  • XmlElement init := method(
  •  
  • // I am the name of the element.
  • self name ::= nil;
  •  
  • // I am the text of the element.
  • self text ::= nil;
  •  
  • // I am the attributes for this element.
  • self attributes ::= XmlAttributes clone();
  •  
  • // I am the child nodes for this element.
  • self childNodes ::= list();
  •  
  • // I am the parent node for this element.
  • self parentNode ::= nil;
  •  
  • // Return self for method chaining.
  • return( self );
  •  
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // I am the XML Parser.
  • XmlParser := Object clone();
  •  
  •  
  • // I am the parser constructor. I keep track of the state of the
  •  
  •  
  •  
  • // I take the name and value and return them as a Map with name and
  • // value keys. This only gets called when the attribute notation
  • // { name: value } is used in the IoML.
  • XmlParser atPutPair := method( name, value,
  •  
  • // Create a new map for this.
  • attribute := Map clone();
  •  
  • attribute atPut( "name", name );
  • attribute atPut( "value", value );
  •  
  • // Return the attribute map;
  • return( attribute );
  •  
  • );
  •  
  •  
  • // I handle curly brackets in the context of the parser only.
  • // For the XML elements, we are going to convert the arguments to
  • // a collection of name-value pairs.
  • XmlParser curlyBrackets := method(
  •  
  • // Create a list to hold our name-value pairs.
  • attributes := list();
  •  
  • // Loop over each bracket argument and get its name-value pair.
  • call message() arguments() foreach( attributePair,
  •  
  • // When evaluating the {name:value} notation, we are using
  • // the addAssignOperator() result. HOWEVER, this cannot
  • // actually be done in the same file as the code which uses
  • // it. As such, we need to use doString() rather than our
  • // typeical doMessage() approach.
  • //
  • // NOTE: I might be completely misunderstanding how to use
  • // the new operator assigment.
  • attributes append(
  • self doString( attributePair asString() )
  • );
  •  
  • );
  •  
  • // Return the parsed attribute list.
  • return( attributes );
  •  
  • );
  •  
  •  
  • // All of the nodes are parsed in the context of the Xml parser. As
  • // such, we need to be able to dynamically handle any element type.
  • // Therefore, we need to listen for method calls that do not have
  • // any natural slot.
  • XmlParser forward := method(
  •  
  • // Get the name of the method that was being invoked in this
  • // "failed" message and will act as our element name.
  • missingMethodName := call message() name();
  •  
  • // Get the missing method arguments. This will contain an
  • // optional attributes collection followed by zero or more
  • // XMl elements.
  • missingMethodArgs := call message() arguments();
  •  
  • // We need to create a new Xml Element with the given name.
  • xmlElement := XmlElement clone();
  •  
  • // Set the name of the element to be the name of the missing
  • // method invocation.
  • xmlElement setName( missingMethodName );
  •  
  • // Now that we have our element, let's check to see if the first
  • // argument passed within the markup was a curly bracket. If it
  • // was, then we are going to treat that as an attribute
  • // collection.
  • if(
  • self isCurlyBrackets( missingMethodArgs at( 0 ) ),
  • (
  • // Pop the first argument off the list so that we can
  • // more easily treat the rest of the arguemnts as nested
  • // element nodes.
  • attributesMarkup := missingMethodArgs removeFirst();
  •  
  • // Parse the attributes as represented by curly brackets.
  • attributesList := self doMessage( attributesMarkup );
  •  
  • // Now, add the attributes to the element.
  • attributesList foreach( attribute,
  •  
  • xmlElement attributes() atPut(
  • attribute at( "name" ),
  • attribute at( "value" )
  • );
  •  
  • );
  • )
  • );
  •  
  • // Loop over each of the remaining arguments to add them as child
  • // nodes on the currrent element.
  • missingMethodArgs foreach( argument,
  •  
  • // Process the arguemnt and get the content.
  • content := self doMessage( argument );
  •  
  • // Check to see if the content is an XML node. If it is, then
  • // we can append it. If it is just a simple value, then we
  • // are going to make it the text of the current node.
  • if(
  • (content type() == "XmlElement"),
  • (
  • // Set the parent node relationship.
  • content setParentNode( xmlElement );
  •  
  • // Append the content as a chlid node.
  • xmlElement childNodes() append( content );
  • ),
  • (
  • // Set the content as the node next.
  • xmlElement setText( content );
  • )
  • );
  •  
  • );
  •  
  • // Return the parsed XML element.
  • return( xmlElement );
  •  
  • );
  •  
  •  
  • // I determine if the given message is a curly brackets notation.
  • XmlParser isCurlyBrackets := method( targetMessage,
  •  
  • // Check to see if the stringifyied version of the message
  • // starts with the curly brackets notation.
  • return(
  • (targetMessage asString() findSeq( "curlyBrackets" )) == 0
  • );
  •  
  • );
  •  
  •  
  • // I parse an XML document written in IoML.
  • XmlParser parse := method(
  •  
  • // Create a new XML document for our parsing.
  • xmlDoc := XmlDocument clone();
  •  
  • // Now that we have our document, we need to start parsing
  • // the elements that will populate it. We are going to assume
  • // that the xml document has only one root node - the first
  • // argument.
  • xmlDoc setRootNode(
  • self doMessage(
  • call message() arguments() at( 0 )
  • )
  • );
  •  
  • // Return the populated XML document.
  • return( xmlDoc );
  •  
  • );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Parse the XML document.
  • girls := XmlParser parse(
  •  
  • girls(
  • { type: "hotties", isActive: "true" },
  •  
  • girl(
  • { id: 17 },
  • name( "Sarah" ),
  • age( 35 )
  • ),
  • girl(
  • { id: 104 },
  • name( "Joanna" ),
  • age( 32 )
  • ),
  • girl(
  • { id: 15 }
  • )
  • )
  •  
  • );
  •  
  •  
  • // Print the girls XML docuement.
  • girls println();

This problem really through me through a loop. I had the hardest time getting ":" to work as an assignment operator. Luckily, in my Googling, I ran into a message thread on the Yahoo Groups site. In the thread, it was stated that an augmented Operator Table cannot act in the same file in which it was changed. I guess this has to do with how the pre-parsing of the file takes place? Really, I have no idea.

Ultimately, I got around this issue by using the doString() method rather than the doMessage() method when evaluating my {name:value} pairs. Something about the doString() method allows the evaluation to take advantage of the updated Operator Table.

When we run the above code, we get the following console output:

<girls isActive="true" type="hotties">
....<girl id="17">
........<name>
............Sarah
........</name>
........<age>
............35
........</age>
....</girl>
....<girl id="104">
........<name>
............Joanna
........</name>
........<age>
............32
........</age>
....</girl>
....<girl id="15">
....</girl>
</girls>

As you can see, all of the XML node nesting and attribute creation worked properly.

The Io language is a lot of fun. There's even some powerful concurrency stuff that I didn't get to play with (think CFThread in ColdFusion). At the end of the chapter Bruce Tate said something that I wholeheartedly agree with - he said that if nothing else, the Io language got him to think differently. As someone who has a fairly decent Javascript background, dealing with this kind of prototype-driven language really does get you to think differently about how objects are related. Obviously, this has all happened so quickly (3 days); but, I think my experimenting with Io is going to change the way that I look at my Javascript code.

Up next: Prolog.




Reader Comments

I like how you completely killed the third assignment. For messages that are methods but don't take any arguments you don't have to add (). In your

  • isCurlyBrackets

you don't have to convert the message to a string you can just do

  • targetMessage name == "curlyBrackets"

.

Reply to this Comment

@David,

Thanks my man. I really wanted to try and take HW3 a step further. Plus, I also think that it was going to make implementing the problem easier, even if much more verbose.

I go back and forth on the methods without parenthesis. I come from a bracket and parenthesis world, so seeing them does make me feel comfortable. However, I also really enjoy seeing things like "println" without parenthesis.

I did notice, however, that sometimes, there seemed to be a confusing order of execution of methods. I may be off on this one, but I think I remember having to actually add some grouping parenthesis in calls like this:

if( foo bar bas == true )

I could swear that I got some errors that said something about the given value not being comparable or something (I making this version up). I did find, though, that if I grouped them:

if( (foo bar bas) == true )

... then it worked fine.

It's like it couldn't figure out if (bas == true) was the message to pass to bar, or if "true" was supposed to compared to the result of "foo bar bas".

But, anyway, I am sure preferences would change the more I used the language.

Thanks for the tip about referencing the targetMessage name. I totally forgot about that! I had a lot of trouble wrapping my head around the "call" object... simply because the name "call" was so confusing. Coming from a Javascript background where "call" is a method on functions, it took me a while to unlearn that and start thinking of it as an object.

Great tip!

Reply to this Comment

Hey Ben,

I was playing around with Io and ran across your post, HW3 is especially interesting. I tried to run your code as is but am getting an exception on ln 309.

Exception: argument 0 to method 'at' must be a Number, not a 'Sequence'
---------
at AttributesWithBuilder.io 309
XmlAttributes atPut AttributesWithBuilder.io 308
XmlParser girls AttributesWithBuilder.io 391
XmlParser parse AttributesWithBuilder.io 389
XmlDocument setRootNode AttributesWithBuilder.io 372

Reply to this Comment

@Dylan,

Hmm, that's odd. It's been a while since I have looked at this (and there's a lot of it). If I had to guess from your error, your base value might be the wrong type? I would assume there is a list/array type data value that has an at(Index) method that takes a number. I also remember that there is an Object/map that has an at(Key) in which the key is a string (sequence). If you are getting a mis-match, perhaps your using a list when you should be using an object?

Sorry I can't be more help.

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.