An Introduction to Io

From Compsci.ca Wiki

Revision as of 14:08, 11 June 2007 by Dan (Talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Contents

Why another programming language?

We've all heard the expression "think outside the box." It means that you shouldn't allow your thinking to be limited by what you're accustomed to.

Every single programming language forces us to think inside a box. Some of the boxes are smaller, and some are larger, but they're still boxes. The only way to gain a broad understanding of programming then, is to learn as many programming languages as we can.

Why this programming language?

Students stumble over two things when learning a programming language. They either have trouble with the syntax or the semantics. How the language looks, or how it behaves.

Syntax is not without importance. Finding easier ways to do things is good. But syntactic conveniences are generally just a sign of support for beneficial semantics.

In functional programming languages we don't say the ability to pass around anonymous functions is useful because of the syntax.

\x -> x * 3 + 16


This is useful because of the utility of being able to quickly generate functions and pass them around as values. It is an idea we can use elsewhere to our advantage. The syntax we'll rarely be able to use.

Io is a programming language with minimal syntactic rules. There is very little to trip over because there just isn't much there. This emphasizes semantics.

So what the heck is Io?

Io is a purely object-oriented programming language. Everything in Io is an object.

What does that mean? Well, simply put, it means the language is consistent.

Io> "hello" size
==> 5
Io> list(1, 2, 3) size
==> 3
Io> "hello" foreach(i, x,
       writeln(i, ": ", x asCharacter)
    )
0: h
1: e
2: l
3: l
4: o

==> Nil
Io> list(1, 2, 3) foreach(i, x,
       writeln(i, ": ", x)
    )
0: 1
1: 2
2: 3

==> Nil
Io> "hello" print
hello
==> hello
Io> 9 print
9
==> 9

A big plus is that Io features an interactive interpreter, making it easy to experiment with code.

And that probably doesn't tell you a whole lot, so let's be a bit more specific.

Hello, world

Io> writeln("hello, world")
hello, world

==> Nil

What's going on here?

Well, "writeln" is a message.

A message has a receiver, and optionally, arguments. At the top level of a program, the receiver is implicitly Object. Therefore we can easily write the following instead.

Io> Object writeln("Hello, world")
Hello, world

==> Nil

The argument in this case is the string, "Hello, world".

When the "writeln" message is sent to Object, it looks up the corresponding method and invokes it with the arguments supplied.

Message passing is not so obvious until you see the same action performed as follows.

Io> perform("writeln", "Hello, world")
Hello, world

==> Nil

Programming Principles

What about math and stuff?

Operators exist, and are quite handy for all of our mathematical needs. They also follow the same precendence rules you're used to.

Io> 1 + 1
==> 2
Io> 2 + 3 * 4
==> 14

So, how does this mesh with the idea of messages and consistency?

Well, let's rewrite the above.

Io> 1 +(1)
==> 2
Io> 2 +(3 *(4))
==> 14

It's just kind of ugly to do it that way.

Variables?

It's not unusual to want to give names to pieces of information.

Io> msg := "Hello, world"
==> Hello, world
Io> writeln(msg)
Hello, world

==> Nil

But what's really going on here?

Io> Object writeln(Object msg)
Hello, world

==> Nil

As with writeln, Object is implicitly there.

Thus "msg" becomes just a value tied to a "slot".

A receiver other than Object?

Io> msg println
Hello, world

==> Hello, world

Here msg is the receiver, and println the message. Of course, the implicit Object is still in there.

Io> Object msg println
Hello, world

==> Hello, world

Perhaps this is easier to understand as follows.

Io> (Object msg) println
Hello, world

==> Hello, world

The println message is being sent to "Object msg". Get used to the version without parentheses, thogh, since that's what you'll see from here on out.

Create your own method

So far we've only seen a very few standard library provided methods. Let's create one.

Io> sayHello := method("Hello" println)
==> method("Hello" println)
Io> sayHello
Hello

==> Hello

There's nothing new here. The := and method are both messages. The following is a more verbose version.

Io> Object sayHello := Object method("Hello" println)
==> method("Hello" println)
Io> Object sayHello
Hello

==> Hello

Let's add an argument.

Io> sayHelloTo := method(name, writeln("Hello ", name))
==> method(name, writeln("Hello ", name))
Io> sayHelloTo("Bob")
Hello Bob

==> Nil

What if the method has lots of arguments?

Io> sayHelloTo := method(
       thisMessage arguments foreach(arg,
          writeln("Hello ", doMessage(arg))
       )
    )
==> method(thisMessage arguments foreach(arg, writeln("Hello ", doMessage(arg))))
Io> sayHelloTo("Bob")
Hello Bob

==> Nil
Io> sayHelloTo("Bob", "John")
Hello Bob
Hello John

==> Nil

So, what's going on here? What is "thisMessage"?

That refers to well, the current message. In the case of:

sayHelloTo("Bob", "John")

It refers to that passing of the sayHelloTo message. The arguments slot of that object contains a list which holds the arguments. It's fairly straightforward.

For that list, we send the "foreach" message. For each argument in that list, evaluate the message. Of course, here we're sending two arguments to "foreach", so the first is the name we're giving to each argument in the list.

So, why can't we just write the following?

writeln("Hello ", arg)

The arguments in the list are not simple values. They're actually objects which represent the messages themselves, before being evaluated. To evaluate those messages and get the resulting value, we can use the doMessage message.

How it all comes together is reasonable straightforward.

A conditional

What if we want it to print "Hello world" if no arguments were passed in?

Io> sayHelloTo := method(
       args := thisMessage arguments
       if(args size == 0,
          "Hello world" println
          return(Nil)
       )
       args foreach(arg,
          writeln("Hello ", doMessage(arg))
       )
    )
==> method(setSlot("args", thisMessage arguments);
if(args size ==(0), "Hello world" println;
return(Nil));
args foreach(arg, writeln("Hello ", doMessage(arg))))
Io> sayHelloTo
Hello world

==> Nil

Here we can see first off the addition of the "args" slot to give a more convenient name to the arguments list.

The next new thing of course is the conditional. If there are no arguments to sayHelloTo, then we first print "Hello world" and then we send the return message with the argument Nil. The return message causes control flow to skip to the end of the method.

It's interesting to note that multiple messages here are considered as a single message by the if message. They are separated by a newline, but could be separated by a semi-colon, as the code after "==>" shows.

Now here we're only providing two arguments to "if". We could provide three.

Io> sayHelloTo := method(
       args := thisMessage arguments
       if(args size == 0,
          "Hello world" println,
          args foreach(arg,
             writeln("Hello ", doMessage(arg))
          )
       )
    )
==> method(setSlot("args", thisMessage arguments);
if(args size ==(0), "Hello world" println, args foreach(arg, writeln("Hello ", d
oMessage(arg)))))

Here the third argument becomes the "else" part of the conditional. An explicit return is no longer necessary since only one of the two messages can be evaluated.

Evaluating messages and a greater appreciation of "if"

Let's implement our own "if" message.

Io> myIf := method(
       args := thisMessage arguments
       if(args size == 2,
          if(doMessage(args at(0)), doMessage(args at(1)))
       )
       if(args size == 3,
          if(doMessage(args at(0)),
             doMessage(args at(1)),
             doMessage(args at(2))
          )
       )
    )
==> method(setSlot("args", thisMessage arguments); if(args size ==(2), if(doMessage(args at(0)), doMessage(args at(1)))); if(args size ==(3), if(doMessage(args at(0)), doMessage(args at(1)), doMessage(args at(2)))))
Io> myIf(4 == 3, "foo" print, "bar" print)
bar
==> bar
Io> myIf(4 == 3, "foo" print, "bar" print; "baz" print)
barbaz
==> baz

It replicates "if" reasonably well, and it's quite simple. So how does this work?

It wouldn't work in many other languages. They strictly evaluate expressions (the rough equivalent of "messages"). If something is passed as an argument to a method/function/etc. it is immediately evaluated, and any side-effects become immediately apparent.

That is something we can't have with a conditional. It has to evaluate only one argument or the other. Fortunately Io makes this simple. Writing our own control structures is therefore a piece of cake.

More flexible conditionals

The conditionals we've seen so far are just peachy... if we want a very simple decision between two possibilities. It looks less nice when we have lots of possibilities.

write("Your name is? ")
name := File standardInput readLine

if(name == "Clarence") then(
   writeln("What a goofy name.")
) elseif(name == "Sid") then(
   writeln("Sure it is...")
) else(
   writeln("Hello ", name)
)

You will note that I didn't just copy and paste this from the interpreter. Instead, I save this in a file named "hello.io", and run it from the command-line with "io hello.io".

Alternatively, you can open your interpreter and send the following message.

doFile("hello.io")

This will run the file. If you've created new methods in that file, they will become available later on in the interpreter.

Adding slots to something other than Object

So far all we've done is give new variables and methods to Object. We could try adding to something else.

Io> Number double := method(self * 2)
==> method(self *(2))
Io> 2 double
==> 4

But how then did "Number" come into being?

Number is just a clone of Object. It then had a bunch of extra stuff tacked on. But, that stuff was tacked onto Number, and not Object. For instance, + is valid for Number, but not for Object.

Io> a := Object clone
==> Object_0074B3B0 do(
  appendProto(Object_003FD640)
)

Io> a + 4


Importer: Object does not respond to '+'

Label                 Line       Char    Message
------------------------------------------------
VMCode                619        18724   raise("Importer", theObject type ..(" do)...
VMCode                623        18927   find(thisMessage name, self)
[command line]        1          3       +(4)


==> Nil
Io> a := Number clone
==> 0
Io> a + 4
==> 4

And yet, Object is a prototype of Number, so Number responds to all of the messages Object does. Those already familiar with object-oriented programming should be thinking "inheritance" about now.

But let's create something new.

Io> Name := Object clone
==> Object_00789D90 do(
  appendProto(Object_003FD640)
)

Io> Name first := ""
==>
Io> Name last := ""
==>
Io> Name fullName := method(first with(" ") with(last))
==> method(first with(" ") with(last))

That looks messy. Let's use the "do" message to clean it up.

Io> Name := Object clone do(
       first := ""
       last := ""
       fullName := method(first with(" ") with(last))
    )
==> Object_0073A9C8 do(
  appendProto(Object_003FD640)
  first := ""
  last := ""
  fullName := Block_0078D3B8
)

Now, we'll want to create new Name objects.

Io> myName := Name clone do(
       first := "Bob"
       last := "Smith"
    )
==> Object_00727DC8 do(
  appendProto(Object_0073A9C8)
  first := "Bob"
  last := "Smith"
)

Io> myName fullName
==> Bob Smith

The myName object has as a prototype Name, and thus responds to the fullName message. Now, what if we want a more formal name?

Io> FormalName := Name clone do(
       title := ""
       fullName := method(
          name := resend
          title with(" ") with(name)
       )
    )
==> Object_00759DC0 do(
  appendProto(Object_0073A9C8)
  title := ""
  fullName := Block_0075A2A8
)

Io> myName := FormalName clone do(
       first := "Bob"
       last := "Smith"
       title := "Mr."
    )
==> Object_00767A88 do(
  appendProto(Object_00759DC0)
  title := "Mr."
  first := "Bob"
  last := "Smith"
)

Io> myName fullName
==> Mr. Bob Smith

Since Name is a prototype of FormalName, we can use the resend message to send the fullName message to Name which gets the combination of the first and last names. We then simply add the title onto the beginning of that.

All sorts of prototype shenanigans

So, what if I have:

Io> myName := Name clone do(
       first := "Bob"
       last := "Smith"
    )
==> Object_00727DC8 do(
  appendProto(Object_0073A9C8)
  first := "Bob"
  last := "Smith"
)

Io> myName fullName
==> Bob Smith

And I want to be able to treat this as a FormalName?

Io> myName do(
       title := "Mr."
       prependProto(FormalName)
    )
==> Object_00742368 do(
  appendProto(Object_00746850, Object_007566B0)
  title := "Mr."
  first := "Bob"
  last := "Smith"
)

Io> myName fullName
==> Mr. Bob Smith

So, what exactly did I do?

I added a title slot to myName, and I prepended FormalName onto the list of prototypes for myName.

Then, when I sent the fullName message to myName, instead of looking in Name for fullName, it found a perfectly suitable version in FormalName and used that instead. I convinced myName that it is in fact a FormalName.

Operators

So, let's say we want to check to see if two names are equal.

Io> Name setSlot("==",
       method(other,
          other first == first and other last == last
       )
    )
==> method(other, other first ==(first) and(other last ==(last)))
Io> a := Name clone do(first := "Bob"; last := "Smith")
==> Object_00724490 do(
  appendProto(Object_007566B0)
  first := "Bob"
  last := "Smith"
)

Io> b := a clone
==> Object_00725BE8 do(
  appendProto(Object_00724490)
)

Io> a == b
==> Bob
Io> b := a clone do(last := "Wilson")
==> Object_00728DC0 do(
  appendProto(Object_00724490)
  last := "Wilson"
)

Io> a == b
==> Nil

Then defining the opposite is a simple matter of simply checking to see if they are not equal. If == returns Nil, then they are not equal.

Io> Name setSlot("!=", method(other, (self == other) isNil))
==> method(other, (self ==(other)) isNil)

Where to Find Io

Io can be downloaded from http://www.iolanguage.com as source code, or precompiled binaries for most operating systems.

Discussion

To Discuss this tutorial visit here.


Credits

Tutorial written by wtd, moved to wiki by TheFerret

Personal tools