Seven Languages in Seven Weeks – (Io Lesson 1)

The Horseman was surprised at the brevity of this lesson. In comparison to Ruby it was a mere stroll through the park. I suspect it has more to do with the brevity of the Io language’s syntax than a lack of topics to talk about. One very interesting thing I noted however was that day 1 for Io touched on topics that very closely relate to Ruby’s metaprogramming capabilities. I’m also going to come right out and say that there’s one particular feature that I really like about Io’s syntax that sets it apart from the ECMA script languages. In ECMA, the = operator is overloaded such as follows

var foo = {}; // dynamic untyped object...
foo.x = 6; // OVERLOADED.  If x exists on foo, assign it 6.  
          //If it does not, create x and assign 6

This kind of overloading is one of the most frustrating things about JavaScript, and why The Horseman vastly prefers never to use dynamic classes in ActionScript. In the absence of a compiler to tell you you’re doing something really stupid, this kind of overloading can easily cause you problems by introducing subtle, hard-to-track bugs related to typos and control-flow problems, and encourages the incredibly tedious overuse of hasOwnProperty() as a sanity check.

Io lays down the law and says “Oh no you don’t.”

foo = 0 
// ERROR! There's no foo slot in the program yet.  You can't assign to it yet.
foo = Object clone 
// ERROR!  Stop trying to assign until you create the foo slot!
// use := instead of =
foo := Object clone
foo x = 0
// WHAT ARE YOU DOING?! There's no x slot on foo!
foo x := 0
// Perfect!  Now you can freely assign with =
foo x = 100
// See?  Isn't that much nicer?

So, with that out of the way…

The assignment was far more open-ended than any of the Ruby tasks. I wonder whether this has more to do with the language or the author’s own lack of familiarity with Io itself?

  • Run an Io program from a file
  • Execute the code in a slot given its name

Given requirements so simple, of course the Horseman had to make something a little bigger of it.

"Hello World" println
Foo := Object clone
Foo sayHello := method("Foo says hello" println)
foo := Foo clone
Foo sayHello
foo sayHello
"First, I will create a count variable on the Foo prototype and 
check its value in the cloned instance."
Foo count := 0
Foo count println // 0
foo count println // 0
"And now I will set it equal to 3 in the prototype and check the instance's value.  Notice that the instance inherits the new value of count!"
Foo count = 3
Foo count println // 3
foo count println // 3
"Here we set the value of count on the foo instance to equal 6."
foo count = 6
Foo count println // 3
foo count println // 6
"Now we set the count value of the prototype Foo to 9 and print 
the value of the instance.  Notice that the instance NO LONGER 
REFERENCES THE Foo prototype's count!"
Foo count = 9
Foo count println // 9
foo count println // 6
"Simple increment (foo count = foo count + 1 .  += is not an 
operator in Io (though I wish += were the operator for creation 
rather than :=  )"
foo count = foo count + 1
foo count println //7
"Simple multiplication (foo count = foo count * 20)" println
foo count = foo count * 20
foo count println // 140

Notice the precedence for what is returned from the various messages we send at the various times. It’s possibly a little surprising at first until it really clicks in your head what’s happening. But this is pedestrian. Surely I can think of something more fun to do with Io… something like this?

trace := method(output, "#{output}" interpolate println)
trace("A trace statement!  This is more like it!")

Aaah, that’s nice. A syntax that feels at least a little more familiar. While Io has very little in the way of operators and keywords of its own, the very interesting thing about this is that it means you’re largely free to create your own domain specific language for use in Io. It’s not going to stop you! So for example if you want some more options in regards to instantiation (maybe for example, with default values) you can create your own constructor. It should surprise absolutely nobody that The Horseman chose the word “new”

GeomPoint := Object clone
GeomPoint x := 0.0
GeomPoint y := 0.0
GeomPoint new := method(newX, newY, 
  obj := GeomPoint clone
  if(newX == nil) then(obj x = 0.0) else(obj x = newX)
  if(newY == nil) then(obj y = 0.0) else(obj y = newY)
GeomPoint coords := method(
  "(x=#{self x},y=#{self y})" interpolate
p1 := GeomPoint new coords println // (x=0,y=0)
p2 := GeomPoint new(10,20) coords println // (x=10,y=20)

We’re now seeing however, the unfortunate side of weakly typed variables and Io’s syntax (though I have to say that they appear to be statically typed given the various errors you receive when attempting incompatible coercions). Just look at all that ridiculous sanity checking to see if the vars are nil before attempting to assign. But no matter, once that’s over you can create new GeomPoint clones easily with new.

So come on, we can do more than just create a GeomPoint. What good is that on its own?

// This feels a bit more familiar...
DisplayObject := Object clone
DisplayObject x := 0.0
DisplayObject y := 0.0
DisplayObject name := "instance"
DisplayObject parent := nil
DisplayObject children := list()
DisplayObject getPosition := method(
  point := GeomPoint new(self x, self y)
// Assign each newly created DisplayObject with a new
// instance name of instanceX with X incrementing
DisplayObject idNumber := 0
DisplayObject new := method(
  obj := DisplayObject clone;
  obj name = "instance#{DisplayObject idNumber}" interpolate
  DisplayObject idNumber = DisplayObject idNumber + 1
  obj parent = nil
  obj children = List clone
// There doesn't seem to be a need for DisplayObjectContainer
// in this context so we'll just roll that into DisplayObject...
// For simply an abbreviated example, let's just have addChild...
DisplayObject addChild := method(displayObject,
  (displayObject type == DisplayObject type) ifTrue (
    if(displayObject parent != nil) then (displayObject parent removeChild(displayObject)) else (nil)
    displayObject parent = self
    self children append(displayObject)
// and removeChild...
DisplayObject removeChild := method(childObject,
  oldSize := children size
  children remove(childObject)
  if(oldSize != children size) then(childObject parent = nil) else(nil)
// And since we can now officially nest DisplayObjects inside
// a display list heirarchy.  Let's give them something to do... like convert
// localToGlobal GeomPoints and globalToLocal!
DisplayObject localToGlobal := method(geomPoint,
  if(parent == nil) then(geomPoint = geomPoint) else(
    geomPoint x = geomPoint x + self parent x
    geomPoint y = geomPoint y + self parent y
    geomPoint = parent localToGlobal(geomPoint) 
DisplayObject globalToLocal := method(geomPoint,
   parList := List clone
   parList append(self)
   tempParent := self parent
   while(tempParent != nil,
      parList prepend(tempParent)
      tempParent = tempParent parent
   parList foreach(i,v, 
      geomPoint x = geomPoint x - v x
      geomPoint y = geomPoint y - v y

So, this should look really familiar to a lot of you. Flash display objects are arranged in a display list heirarchy. Now these Io DisplayObjects can do the same. Also, you can convert points from global into local space and back again.

mainDisplayObject := DisplayObject new
subDisplayObject := DisplayObject new
mainDisplayObject addChild(subDisplayObject)
trace(subDisplayObject parent)
trace(mainDisplayObject children at(0))
subDisplayObject x = 50
trace("Expecting local to global of x50 y0")
trace(subDisplayObject localToGlobal(subDisplayObject getPosition) coords)
mainDisplayObject x = 50
mainDisplayObject y = 25
resultPoint := GeomPoint new
trace("Expecting local to global of x100 y25")
resultPoint = subDisplayObject localToGlobal(subDisplayObject getPosition)
trace(resultPoint coords) 
// Look familiar?  Notice we get a return of GeomPoint from localToGlobal!
// for the rest, I'll use lazy shorthand (and break the rule of never putting code
// that results in actual execution inside a trace statement).
trace("Global point x0,y0 should be the exact negative of mainDisplayObjects 50/25")
trace(mainDisplayObject globalToLocal(GeomPoint new()) coords)
trace("Global point x0,y0 should be the exact negative of the cumulative 100/25 for this")
trace(subDisplayObject globalToLocal(GeomPoint new()) coords)
trace("Global point x125,y0 should traslate to x25,y35 here.")
trace(subDisplayObject globalToLocal(GeomPoint new(125, 60)) coords)

All the tests passed muster. So what about the add/remove child methods?

trace("Who is subDisplayObject's parent?")
trace(subDisplayObject parent)
mainDisplayObject removeChild(subDisplayObject)
trace("After calling mainDisplayObject remove(subDisplayObject) who is sub's parent?")
trace(subDisplayObject parent)
trace("And does main have children?")
trace(mainDisplayObject children)
subSubDisplayObject := DisplayObject new
trace("create a new DisplayObject and add it as a child to subDisplayObject...")
subDisplayObject addChild(subSubDisplayObject)
trace("and add subDisplayObject back as a child to main...")
mainDisplayObject addChild(subDisplayObject)
trace("Now, make main add the newest clip as its own child.")
mainDisplayObject addChild(subSubDisplayObject)
trace("The result is that main should have two children now, and both children should have main as parent")
trace("main's children")
trace(mainDisplayObject children)
trace("sub's children")
trace(subDisplayObject children)
trace("subSub's children")
trace(subSubDisplayObject children)
trace("sub's parent")
trace(subDisplayObject parent)
trace("subSub's parent")
trace(subSubDisplayObject parent)

All the resulting traces were exactly as expected in my implementation.

I’m quite aware that there’s at least one bug in the addChild implementation (trying to add a parent as a child of a grandchild could get a little wiggy), but the goal isn’t to perfectly replicate Flash. Rather, the goal is to show how comparatively easy it is to build up a language around Io, and how it generally stays out of your way while you do it. Naturally, this implementation also lacks a crucial graphics component.

Alas, we can’t have everything.

  1. No comments yet.