Chris Umbel

Io, A Beautiful, Prototype-Based Language

I recently started reading Seven Languages in Seven Weeks by Bruce A. Tate and intended to ultimately post a review. Something went wrong along the way, however. On the second language covered in the book I became so intrigued that I had to dedicate a single post to it immediately. That language is Io, a prototype-based language created by Steve Dekorte.

Now, you have to keep in mind that I've only been hacking Io for about three days. I'm not an expert. I can't promise any of the examples I'm going to provide are idiomatic, robust or performant. I can't promise any of my advice will be prudent or that this article can even properly tutor you on Io.

I can, however, make the strong recommendation that you give Io a shot especially if you're a novice JavaScript programmer. Being prototype-based and syntactically simple Io can help an aspiring JavaScripter truly understand the patterns without the baggage that comes along with JavaScript. I can only promise an honest attempt at whetting your appetite.

Here I'll just outline some examples rather than deliver thorough instruction. For that Seven Languages in Seven Weeks does a fine job (most of these examples are adapted from its exercises) and the Io Guide can be a wonderful help as well.

Prototype-based

As I mentioned several times already Io is prototype-based. There are no classes yet there are objects. Objects can clone other objects providing an inheritance mechanism. Consider the following example.

Thing := Object clone

That code created a prototype named "Thing" from the super-prototype "Object". Note the ":=" assignment operator. That essentially creates the destination if it does not exist yet. Had "Thing" already been assigned to "=" would have sufficed as it can only assign an existing slot (slots are the named locations in an object where data members and methods can be stored).

Now lets add a method to the "Thing" prototype. The following code will add a method into the "printMessage" slot of the "Thing" prototype.

Thing printMessage := method(
    writeln("Hello, thing!")
)

We can clone the "Thing" prototype into an instance and call the "printMessage" as such.

thing := Thing clone
thing printMessage

which would output

Hello, thing!

Io thinks of things in terms of message-passing. Rather than saying, "we just called thing's printMessage method" we should say, "we just sent a printMessage message to thing."

Note that we cloned into a lower-case "thing". When you're defining a pure prototype you start the identifier with a capital and instances with lower case.

Now lets add a data member to the instance and method to the prototype to demonstrate parameters and encapsulation.

Thing calculatePrice := method(markup,
    self price + markup
)

thing price := 10
thing calculatePrice(2) println

which outputs

12

That example also demonstrates chaining messages together. calculatePrice returned a Number and we in turn pass a println message to the number (as an alternative to passing the number itself to writeln).

Metaprogramming

One of the exercises covered in Seven Languages in Seven Weeks that I found interesting was changing the behavior of Io itself so that division by zero would result in a 0 not infinity. Of course, you'd probably never want to do that, but it's a wonderful example of how much control you have over the runtime itself.

For instance if you execute the following in Io without any additional intervention

(6 / 0) println

you get

inf

Now let's reach into Io and change how division works.

Number oldDiv := Number getSlot("/")

Number / := method(d,  
  if(d == 0, 0, self oldDiv(d))
)

(6 / 2) println
(6 / 0) println

outputting

3
0

The first output was done just to illustrate that we didn't break division entirely. The second illustrates that we sure did change how division by 0 works, rather than "inf" we got "0".

Let's break it down line by line.

Number oldDiv := Number getSlot("/")

That may look reasonable to a Rubyist who would "alias" in a situation like this. We're basically copying the division operator's (the method in slot "/") logic into another slot named "oldDiv". Since we're going to rewrite "/" we'll want to keep the functionality around for later use and "oldDiv" is a fine place.

Number / := method(d,  
  if(d == 0, 0, self oldDiv(d))
)

Now we've changed the "/" method of Number. If the denominator (the lone parameter) is zero we will return zero. Otherwise we rely on the "oldDiv" to perform normal division.

DSLs/Structured Data Parsing

As a Rubyist by trade I always chuckle when I hear .Net or Java programmers claim that Domain Specific Languages are a waste of time. From their perspective the time investment is far greater than any value that might be yielded. Nine times out of ten they're probably right. They work with tools with a fixed idea of what instructions should look like.

Io gives you a great deal of control over the language's parser itself. Rather than writing a parser or implementing a DSL within the confines of the host language you can teach Io to parse and evaluate your DSL within its own interpreter! In Io the line between internal and external DSLs is somewhat blurred and I think that's just fantastically nifty.

We're going to write a little JSON parser here. Sure, there are probably better ways of parsing JSON with Io but it provides an effective example that's easy to relate to.

Take a file named "test.json" with the following content as given:

{
  "name": "Chris Umbel",
  "lucky_numbers": [6, 13],
  "job"	: {
    "title": "Software Developer"
  }
}

The following code will parse the JSON data, albeit liberally. This is meant to be demonstrative, not robust.

OperatorTable addAssignOperator(":", "atPutNumber")

curlyBrackets := method(
  data := Map clone
  call message arguments foreach(arg,
    data doMessage(arg))
  data
)

squareBrackets := method(
  arr := list()

  call message arguments foreach(arg,
    arr push(call sender doMessage(arg)))

  arr
)

Map atPutNumber := method(
  self atPut(
    # strip off leading and trailing quotes
    call evalArgAt(0) asMutable removePrefix("\"") removeSuffix("\""),
    call evalArgAt(1)
  )
)

s := File with("test.json") openForReading contents
json := doString(s)

json at("name") println
json at("lucky_numbers") println
json at("job") at("title") println

which will output

Chris Umbel
list(6, 13)
Software Developer

Now to break it down.

OperatorTable addAssignOperator(":", "atPutNumber")

Here we told Io to accept a brand spanking new assignment operator with the text ":". It will then pass the argument along to the target object via the "atPutNumber" message.

curlyBrackets := method(
  data := Map clone
  call message arguments foreach(arg,
    data doMessage(arg))
  data
)

That instructs Io what to do when it comes across a curly bracket when parsing code. In our case it it creates a Map and begins to fill it. "call" performs reflection on the argument data passed to the method. "call message arguments" accesses the list of all arguments recieved.

squareBrackets := method(
  arr := list()
  call message arguments foreach(arg,
    arr push(call sender doMessage(arg)))
  arr
)

Here we instructed Io how to deal with JSON arrays. Per the slot name it builds a list of all elements enclosed in square brackets.

Map atPutNumber := method(
  self atPut(
    # strip off leading and trailing quotes
    call evalArgAt(0) asMutable removePrefix("\"") removeSuffix("\""),
    call evalArgAt(1)
  )
)

The Map prototype has been given a atPutNumber method that will strip quote off of the element names and slap the value specified by the JSON into the corresponding value in the Map. "evalArgAt" grabs the argument data at a specified index.

s := File with("test.json") openForReading contents
json := doString(s)

Now there's the money. We loaded up the JSON document and slapped its contents in a string. We then essentially eval it with "doString" letting the Io interpreter do the dirty work.

Message Forwarding

In Ruby something called "method_missing" is relied upon to handle methods of arbitrary names at runtime. The Io equivalent is "forward". The following example supplies a simple mechanism to build XML like:


  
    
      The Thing
    
    
      Horror
    
  

with code like:

builder := Builder clone
builder movies(
  movie(
    title("The Thing"),
    genre("Horror")
  )
)

Here's the code to make it happen.

Builder := Object clone do(
  depth := 0
)

Builder indent := method(
  depth repeat(
    write("  ")
  )
)

Builder emit := method(
  indent

  call message arguments foreach(arg,
    write(call sender doMessage(arg))
  )

  writeln
)

Builder emitStart := method(
  emit("<", call evalArgs join, ">")
)

Builder emitEnd := method(text,
  emitStart("/", text)
)

# handles messages for non-existant methods. 
Builder forward := method(
  emitStart(call message name)
  depth = depth + 1

  call message arguments foreach(arg,
      content := self doMessage(arg)
      if(content type == "Sequence",
        emit(content)
      )
  )

  depth = depth - 1
  emitEnd(call message name)
)

builder := Builder clone
builder movies(
  movie(
    title("The Thing"),
    genre("Horror")
  )
)

Now that's a big example, but let me extract the section of most relevance.

# handles messages for non-existant methods. 
Builder forward := method(
  emitStart(call message name)
  depth = depth + 1

  call message arguments foreach(arg,
      content := self doMessage(arg)
      if(content type == "Sequence",
        emit(content)
      )
  )

  depth = depth - 1
  emitEnd(call message name)
)

By implementing a method in the "forward" slot we allow the Builder prototype to accept messages of any name. Our "forward" method then obtains the name of the message sent with "call message name" and handles it accordingly (making a node of that name).

Next Steps

One thing not covered here is the concurrency story, and Io really shines there. Easy coroutines, actors and futures are available to provide refreshing simplicity to what's typically a tricky problem.

There are also reasonable libraries available for common tasks like networking and XML parsing.

Check out the Io Guide for information on these and other topics. Of course Seven Languages in Seven Weeks is wonderful as well.

Tue Sep 06 2011 03:00:00 GMT+0000 (UTC)

Follow Chris
RSS Feed
Twitter
Facebook
CodePlex
github
LinkedIn
Google