14 Languages - Io
2015-03-29
The second language - Io
Introduction
Io is a prototype based language. What does that mean then? It means that the only way to create an object is to create a clone of another object. The object that is cloned is called the prototype. There aren't any classes in Io. Only objects.
A central concept in Io is a message. A message can be sent to an object (which when receiving a message is called a receiver), which then responds to the message by returning a receiver (itself or another object), which then can be the target of another message and so on. The whole Io language is based on chaining receivers together by means of messages. The object sending the message to the target
is called the sender
.
Another central concept is that of a slot. A slot is similar to an entry in a hash table. It has a name (comparable to the key of the hash table entry) and, possibly, a value. An object can contain an unspecified number of slots and new slots can be added on the fly, so in this sense an object is like a hash table. In fact, there isn't much more to an object than it being a collection of slots. Sounds a little boring? Maybe, but one can accomplish quite a lot with just a set of slots.
So, what can be put in a slot? The most obvious thing to put in a slot is of course some piece of data, e.g. a string. The value of the slot can then be accessed by sending a message containing the name of the slot to the object. But a data value is not all that can be put in a slot. It turns out that also methods can be stored in slots. It is by putting methods in the slots of an object and then sending a message contaning the name of the method to the object that methods are invoked.
Executing Io programs
An Io program is executed by running the interpreter io
that comes with the Io installation bundle. After having started the interpreter a prompt looking like this will be displayed:
Io 20110905
Io>
Now programs can be entered at the prompt.
Another way of executing an Io program is to give the name of a file containing an Io program as a parameter to the io executable: io myProgram.io
.
Basic Syntax
Comments
Comments can be created using //
, /**/
or #
syntax.
Hello World!
Hello World is as simple as to send the println
message to the "Hello, World!"
object:
"Hello, World!" println
...
Hello, World!
Listing 1: Hello World in Io.
Creating objects
To create an object to start working with, one can be cloned from the Object
prototype. From that object, other objects (both types and other objects) can then be created. To create a type, the name of the clone should start with a capital letter. Regular, non-type objects have names starting with a lowercase letter.
Animal := Object clone
Dog := Animal clone
fido := Dog clone
Listing 2: Cloning objects to create other objects.
Slots
Instead of having variables, Io has, as I mentioned above, slots. A slot can be created and assigned using the :=
operator. If a slot already exists, a value can be assigned to it using the =
operator. The ::=
operator is like :=
but in addition to creating a slot and assigning to it, it also creates a setter method for the slot.
When assigning values to slots, the following syntax is used objectOrType nameOfSlot := value
An example:
MyType := Object clone
MyType mySlot := "This is a value for a slot in a type"
MyType mySlot println
myObject := Object clone
myObject slotInObject := "This is a value for a slot in an object"
myObject slotInObject println
...
This is a value for a slot in a type
This is a value for a slot in an object
Listing 3: Assigning values to slots in types and objects..
Something that confused me at first when I was looking at example programs that I found on the net, was assignments to slots where the object or type of he slots wasn't specified. Like this: a_slot := 42
. When I experimented a bit, it turned out that when using that syntax, the type Object
is implicit. I.e. if no object or type is specified when assigning or accessing a slot, the slot is assumed to belong to Object
. This syntax is quite handy for program parts where a regular variable should be used in a conventional programming language. An example:
a := 1
b := 2
c := a + b
"In Object a has the value: " print;Object a println
"In Object b has the value: " print;Object b println
"In Object c has the value: " print;Object c println
...
In Object a has the value: 1
In Object b has the value: 2
In Object c has the value: 3
Listing 4: When no object or type is specified, it defaults to Object.
In listing 4 above, I've used semicolons to separate expression instead of having each expression on a separate line. This was done to group the expressions in a more logical way (and had the nice side effect of showing the usage of ; as an expression separator ;)
Methods
Methods are defined by creating a method
object and assigning it to a slot of an object. The name of the slot becomes the name of the method. To invoke the method on the object send the name of the method/slot to the object. An example:
Vehicle := Object clone
Vehicle description := method("Driving...." println)
Vehicle description
...
Driving....
Listing 5: Creating and invoking a no-arg method..
To define a method that takes parameter, specify the names of the parameters as the first arguments in call to method
:
Multiplier := Object clone
Multiplier timesTwo := method(n, n * 2)
Multiplier timesTwo(4) println
Multiplier multiply := method(n, m, n * m)
Multiplier multiply(7, 8) println
...
8
56
Listing 6: Defining methods taking arguments.
As can be seen from the example above, when calling a method, the arguments are specified in a comma separated list enclosed in parenthesis.
Lists
A list
is an ordered collection of objects. In Io all lists have List
as (one of) their prototypes. A list can be created by using the lists()
method on Object
:
a_list := list("a", "b", "c", "d")
a_list println
...
list(a, b, c, d)
Listing 7: Creating a list.
The List
class supports a lot of different methods. For example, an element can be added using the append()
method:
a_list append("e")
a_list println
...
list(a, b, c, d, e)
Listing 8: Appending to a list.
A value in a list can be retrieved using the at()
method and it can be set using the atPut()
method.
b_list := list(1,2,3)
b_list at(1) println
b_list atPut(1, 42)
b_list at(1) println
...
2
42
Listing 9: Getting and setting values in a list.
Other methods List
:s support are for example: size()
, pop
, at()
, prepend()
, sum()
, average()
and isEmpty()
.
Maps
Io also supports a hashtable like datastructure: Map
. Basic usage of a Map
can be seen in the example below:
aMap := Map clone
aMap atPut("a", "The letter a")
"The element for key a" println
aMap at("a") println
"
Now adding a value for key b" println
aMap atPut("b", "The letter b")
"
Printing the map as a list..." println
aMap asList println
"
...and as an object:" println
aMap asObject println
"
The number of elements in the map is:" println
aMap size println
"
The keys of the map are:" println
aMap keys println
Listing 8: Basic usage of Map.
If clauses
There are a couple of different ways in which an if clause can be constructed. The first way is to use the if()
construct taking two (no else clause) or three (else included) parameters. The first parameter is the boolean expression on which the if
clause should be based, the other parameter is the message to evaluate if the boolean expression is true
, and the third parameter represents the message to evaluate if it was false. An example:
if(7 < 42, "7 less than 42" println, "7 greater than 42" println)
...
7 less than 42
Listing 10: If clause, variant 1.
The second way to create an if clause is to the if() then() else()
construct. Here the boolean expression goes in the if()
method, the message to evaluate in the true branch in the then()
and the message to evaluate in the false branch in the else()
(which of course is optional). The previous example rewritten to use this construct looks like this:
if(7 < 42) then("7 less than 42" println) else("7 greater than 42" println)
...
7 less than 42
Listing 11: If clause, variant 2.
There also exists an elseif()
construct:
if(87 < 42) then("87 less than 42" println) elseif(87 == 42) then("87 equal to 42" println) else("87 greater than 42" println)
...
87 greater than 42
Listing 12: If clause containing both an elseif() and a else() clause.
The third variant of an if clause is the ifTrue()
and ifFalse
constructs:
(87 < 42) ifTrue("87 less than 42" println) ifFalse("87 greater or equal to 42" println)
...
87 greater or equal to 42
Listing 13: If clause, variant 3.
An important thing to notice is that the code to execute in one of the branches does not have to fit in a single line. An (almost I suppose...) unlimited number of messages can be constructed by separating them using semicolons or by putting them om separate lines. An example:
if(7 < 42,
"We all know that 7 is less than 42..." println
"...but I want to stress the fact anyway." println
)
...
We all know that 7 is less than 42...
...but I want to stress the fact anyway.
Listing 14: If clause with a multi-message code section in its true branch. Messages separated by a newline.
The program above could equally well have been written using the message separator ;
if(7 < 42,
"We all know that 7 is less than 42..." println ; "...but I want to stress the fact anyway." println
)
...
We all know that 7 is less than 42...
...but I want to stress the fact anyway.
Listing 15: If clause with a multi-message code section in its true branch. Messages separated by a semicolon.
When using the if clause with an else part and where the true/false sections contain multiple messages separated by ; one has to be extra cautious not to mix up the comma separating the true branch from the false branch and the semicolons separating the messages. In these situations, where both the true branch and the false branch contain multiple messages, it is probably better to separate the messages with newlines instead of semicolons. When I think about it, it is probably almost always better to separate the messages with newlines instead of semicolons ;)
Loops
Io supports the following looping constructs:
loop
: loops infinitely.repeat
: loops a specified number of times.for
: loops over an integer interval, possibly with a step.while
: loops until a boolean expression evaluates to false.
i := 0
loop(
i println
i = i + 1
if(i>=3, break)
)
...
0
1
2
Listing 16: Example of using loop.
In the example above, we also see the usage of break
, which, as usual, breaks out of the innermost loop. Io also supports continue
.
3 repeat("Hello!" println)
...
Hello!
Hello!
Hello!
Listing 17: Example of looping using repeat.
for(i, 1, 10,
i println
)
...
1
2
3
4
5
6
7
8
9
10
Listing 18: Example of the for construct not using an explicit step (=> step of 1 is used.).
An explicit step can also be specified for for
loops.
for(i, 1, 10, 2,
i println
)
...
1
3
5
7
9
Listing 19: Example of a for loop having an explicit step.
i := 0
while(i < 3,
i println
i = i + 1
)
...
0
1
2
Listing 20: Example of a while loop.
Message Reflection
As was told before, message passing in Io is build on sender
:s sending messages to targets
that executes the message. The retrieve metadata about a message, the sender of the message or the receiver of the message the call
function can be used.
call
can be used in these forms (among others):
call sender
- retrieve the sender the message currently being processed.call target
- retrieve the target the message currently being processed.call message name
- retrieve the name of the message currently being processedcall message arguments
- retrieve the arguments of the message currently being processed.
An example program to demonstrate the usage of call
can be seen in the listing below:
# Sets up a chain of three objects: right - middle - left, where a message is sent from
# the right hand object, through the middle object to the left hand object.
# The purpose is to illustrate the usage of the call function to retrieve metadata about
# the sender, target and the message itself.
# Create the objects.
rightHandObject := Object clone
middleObject := Object clone
leftHandObject := Object clone
# Create a method that should receive the message and print it.
leftHandObject receive := method(arg,
"LHO received: " print
arg println)
# The middle object should forward the message after having used call to print metadata about it.
middleObject objectToSendTo := leftHandObject
middleObject passThrough := method(txt,
"*** In MiddleObject.passThrough" println
"*** Sender" println
call sender println
"*** Target" println
call target println
"*** messge name" println
call message name println
"*** messge args" println
call message arguments println
self objectToSendTo receive(txt))
# The right hand object should start the message chain.
rightHandObject objectToSendTo := middleObject
rightHandObject send := method(txt,
self objectToSendTo passThrough(txt))
# Initially, we print some info about the objects so that we can compare the hashcodes of the
# objects with the metadata printed by the middle object. This way we can figure out which
# objects that are returned by call sender and call target.
"***********" println
"** RHO: " println; rightHandObject println
"** MiddleObject: " println; middleObject println
"** LHO: " println; leftHandObject println
"***********" println
# Start the message passing by sending a message to the right hand object.
textMessage := "Hello"
"Sending " print; textMessage print; " to rightHandObject" println
rightHandObject send(textMessage)
Listing 21: Using call to retrieve metadata..
Output from a run of the program can look like this:
***********
** RHO:
Object_0x24e41d0:
objectToSendTo = Object_0x241ed40
send = method(txt, ...)
** MiddleObject:
Object_0x241ed40:
objectToSendTo = Object_0x241ed00
passThrough = method(txt, ...)
** LHO:
Object_0x241ed00:
receive = method(arg, ...)
***********
Sending Hello to rightHandObject
*** In MiddleObject.passThrough
*** Sender
Object_0x24e41d0:
objectToSendTo = Object_0x241ed40
send = method(txt, ...)
*** Target
Object_0x241ed40:
objectToSendTo = Object_0x241ed00
passThrough = method(txt, ...)
*** messge name
passThrough
*** messge args
list(txt)
LHO received: Hello
Listing 22: Output from the program illustrating the usage of call.
We see that, as expected call sender
returns the right hand side object and call target
return the middle object (which is executing the call target
). By using call message name
and call message arguments
we can retrieve the name of the called method (passThrough
) and its arguments.
Object Reflection
Object reflection involves examining the slots and the prototypes of an object (or type). The slots of an object can be retrieved by sending the slotNames
message to the object. Which prototypes an object has can be found out using the proto
message.
The following example demonstrates how slotNames
and proto
can be used.
# Add a method that recursively prints the names of the slots of this type and its prototypes to Object. When the recursion reaches the level just before Object it is stopped.
Object printSlots := method(
# Print the slots of this object
"The slots of " print
self type print
" are: " print
self slotNames foreach(slotname,
slotname print
", " print
)
"" println
# Recursively call printSlots() for all prototypes.
# The prototype of Object is Object => we have to check
# if the prototype is Object to avoid infinite recursion.
if (self proto != Object, self proto printSlots())
)
# Create a new type having one slot containing data and one slot containing a method.
MyType := Object clone
MyType mySlot := "This is a value for a slot in a type"
MyType aMethod := method("This is a method" println)
# Create another type by cloning the first type.
AnotherType := MyType clone
# Print the slots of the second type (which recursively will print the slots of its prototypes (minus Object)).
AnotherType printSlots()
Listing 23: Usage of slotNames and proto.
Features
The concurrency support in Io seems rather nice. Let's delve a little bit into that.
Coroutines
A coroutine is a function that voluntarily suspends, or yields, its own execution to let some other piece of code (which most likely is a coroutine as well) run. When the other code piece has finished, the function resumes its execution and runs until it terminates or yields again. This way, the execution can switch back and forth between a number of functions that will cooperate to produce the final result of the program.
A function in Io can yield the execution by using the yield
keyword.
Another piece of the Io language that one has to know about when playing around with coroutines, is that messages can be sent asynchronously to an object by prepending either @ or @@ to the name of the message when sending it. The difference between @@ and @ is that the former returns nil
and the latter a future (see the section on futures below). If a message is sent synchronously to a coroutine, there will be no other coroutine executing when yielding, so one has to use asynchronous message passing when playing around with coroutines. Let's try it out.
The following is a small program containing two objects each having a method that writes two strings to standard out. Between the two write statements, there is a yield
that lets another concurrently executing method have a go. In the first version of the program, we invoke the two methods synchronously:
man1 := Object clone
man1 talk := method(
"Man 1 says hello!" println
yield
"Man 1 says nice to meet you" println
)
man2 := Object clone
man2 talk := method(
"Man 2 says hello!" println
yield
"Man 2 says nice to meet you" println
)
man1 talk; man2 talk
...
Man 1 says hello!
Man 1 says nice to meet you
Man 2 says hello!
Man 2 says nice to meet you
Listing 24: Synchronous invocation of coroutines. Not much point in doing this.
As we see, the output is as expected: first the first function executes, then the other function executes.
If we change the invocation to be asynchronous however, the result is different:
man1 := Object clone
man1 talk := method(
"Man 1 says hello!" println
yield
"Man 1 says nice to meet you" println
)
man2 := Object clone
man2 talk := method(
"Man 2 says hello!" println
yield
"Man 2 says nice to meet you" println
)
man1 @@talk; man2 @@talk
Coroutine currentCoroutine pause
...
Man 2 says hello!
Man 1 says hello!
Man 2 says nice to meet you
Man 1 says nice to meet you
Listing 25: Asynchronous invocation of coroutines. Interleaved execution.
Now the execution of the two methods were interleaved. We also see that in this particular execution man2 gets to execute first, but this is just a coincidence. It could equally well have been the other way around.
We've used the Coroutine currentCoroutine pause
to make the program wait until all asynchronous methods have finished executing. Without it, the main program could have terminated first, before the two methods, and then there would have been no output, which is kind of boring.
Actors
An actor is a concept used in computer science to model a concurrent entity. When receiving a message, an actor can perform business logic (e.g. change its own state), create more actors and send messages to other actors. What it cannot do is to change the state of other actors. It communicates with other actors by sending messages to them. This is the advantage that using actors instead of threads for concurrent programming. Threads may share state, which may lead to all kinds of problems, eg. race conditions and deadlocks.
So how does one create an actor in Io? By sending a concurrent message to a regular object! Simple, isn't it? Let's exemplify.
We create two objects each having a method that waits for a certain amount of time before printing a message. When we call the messages synchronously, we see that as expected, the methods execute in order:
slower := Object clone
slower run := method(
wait(2)
writeln("In slower")
)
faster := Object clone
faster run := method(
wait(1)
writeln("In faster")
)
slower run(); faster run()
...
In slower
In faster
Listing 26: Invoking synchronously => regular object.
When we invoke the methods synchronously, however, the two objects become actors, and the faster object terminates its execution more quickly than the slower one:
slower := Object clone
slower run := method(
wait(2)
writeln("In slower")
)
faster := Object clone
faster run := method(
wait(1)
writeln("In faster")
)
slower @@run();faster @@run()
wait(3)
...
In faster
In slower
Listing 27: Invoking asynchronously => actor.
Futures
A future is an object returned from an asynchronous invocation of a method. The future is returned from the method directly when the method starts executing not, as a regular result object, in the end of the method when its execution is about to end. A future contain a synchronous method to retrieve that final answer, however. An invocation of that will not return until the original method has completed its work and returned the result. This means that a program can invoke a method, store the future returned by that method, perform some other task and, when that task is finished, retrieve the result computed by the method by invoking the synchronous method on the future. This way the program can perform useful work at the same time as the method executes instead of just waiting for the method to terminate. Smart, isn't it?
An example:
futureResult := URL with("http://www.larsnohle.se") @fetch
writeln("This will execute at once")
# Perform useful work here!
resultSize := futureResult size
writeln("Has got the result. The size of it was: ", resultSize)
...
This will execute at once
Has got the result. The size of it was: 1838
Listing 28: Using a future to retrieving the result of an asynchronous method call.
The message This will execute at once will be printed right away. Then some time will pass before the web page will be retrieved and the second message will be printed. In this program no useful work is performed after the call to @fetch, but in a real world program some calculation could have been performed here.
Metaprogramming
Io, of course, contains many other features as well. One of them is support for metaprogramming by redefining the forward
message of Object
.
forward
can be use to pick up messages to an object that the object normally wouldn't handle. As an example, we write a small program that redefines forward
so that the name of the non-existing message will be printed, and so will also any string arguments of that message.
Greeter := Object clone
Greeter forward := method(
write("The name of the message is: ")
call message name println
call message arguments foreach(arg,
content := self doMessage(arg)
if (content type == "Sequence",
write("Found a string argument: ")
content println)
)
)
greeter := Greeter clone
greeter say("Telefonbanken")
...
The name of the message is: say
Found a string argument: Telefonbanken
Listing 29: Redefining forward
Gotchas
Here are some things that tricked me while writing some example programs in Io:
- Repeatedly using
=
instead of:=
- Trying to separade statements to execute in an
if
clause using commas instead of newlines.
FizzBuzz in Io
My, non-optimal, implementation of the FizzBuzz kata in Io looks like this:
for(i, 1, 100,
if(i % 3 == 0,
"Fizz" print
)
if(i % 5 == 0,
"Buzz" print
)
if(i % 3 != 0 and i % 5 != 0,
i print
)
"" println
)
Listing 30: FizzBuzz implementation in Io.
What I like about Io
Io is an interesting language. The syntax is strange until you get the hang of it. I've never programmed in a language that is anything like it. This makes it rather fun to use Io. It feels a little bit like you are playing an adventure game trying to explore a world that has just been discovered. It's great fun!
I also like the smallness of the language. It seems that the core language is quite small. There are a very limited number of language constructs that one has to learn. Once you have learned those, I think that the language will not give you very many suprises regarding its syntax.
On the technical side, I like the concurrency support of Io. It it extremely easy to send an asynchronous message instead of a synchronous message to an object (just prepend @ or @@ to the message name). Prototype based inheritance is also interesting. I'm not sure that I like it really, but it is something...different, which makes it interesting. At least in the beginning.
What I don't like about Io
It is quite hard to find information about Io. There are the official home page, which contains, how shall I put it, brief descriptions about most of the Io syntax. The problem is that the descriptions are so brief that if one wishes to do something just a little bit out of the ordinary, the chances are that that isn't covered by the documentation. Then the second problem kicks in: it is not easy to find any other resource regarding Io on the Net. Googling for an Io related topic doesn't will not give you very many hits. And the hits found will probably be either links to the official home page, or to some blog where a solution to one of the exercises in the Seven Languages in Seven Weeks book are discussed (which of course is helpful if you are struggling with with those...)
Onwards, upwards
The time it takes to write these blog entries about the "14" languages takes more time than I had anticipated. Something always comes up that I have to do before I can continue reading/trying out the language/writing about it: work, life, another topic to write about. Therefore I think I have to revise my original plan of going through all 14 languages in the first six months of 2015. My aim now is going through the first seven languages in the whole of 2015. I slight modification of the plan... But hopefully it is more realistic. If I get around to doing more than seven, it's good, but I won't be disappointed if I don't. Well, well. We'll so how it goes.
Now on to the next language: Prolog! But first it seems that I have to study up a little bit on Docker...
Previous parts in the series
1. RubyLinks
[1] Io Programming Guide[2] Wikibooks: Io Programming
[3] Explaination of Actors.