The Io Programming Language
1.0
Copyright 2005 Steve Dekorte
Usage of the works is permitted provided that this
instrument is retained with the works, so that any entity
that uses the works is notified of this instrument.
DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.
Contents
Simplicity is the essence of happiness.
- Cedric Bledsoe
Io is a dyanmic prototype-based programming language. The ideas in Io are mostly inspired by Smalltalk[1] (all values are objects), Self[2] (prototype-based), NewtonScript[3] (differential inheritance), Act1[4] (actors and futures for concurrency), LISP[5] (code is a runtime inspectable / modifiable tree) and Lua[6] (small, embeddable).
Why Another Language?
To explore the idea that conceptual simplification leads to greater flexibility and power.
Design Goals
Io’s goal is to be a language that is:
easy to use
conceptually simple and consistent
easily embedded
useful
multi-platform
capable of desktop, server and embedded scripting applications
Design Guidelines
It Just Works
You don’t need to be a system administrator to install Io or need to set environment variables to use it. Io applications don’t require installers and are not path dependent. The goal i s that much as possible, things should "just work" out of the box .
Bindings Are Good
Many language communities view code outside the language as something to be avoided. Io embraces the idea of using C bindings for performance sensitive features (graphics, sound, encryption, array processing, etc) while maintaining multi-platform support by encouraging the use of platform independent or multi-platform C libraries (OpenGL, PortAudio, etc).
In order to provide a common base on which to build applications, the Io project also maintains official bindings for networking, graphics, sound, etc. that are included in the full distribution.
Objects are Good
When possible, bindings should provide an object oriented interface instead of mimicking low-level C APIs. Also, concrete design is favored over the abstract. Dozens of classes should not be required to do a simple operations.
Io distributions are available at:
To build, from the top folder, run:
make
Binaries will be placed in the binaries subfolder. To install:
make install
and to run the unit tests:
make test
An example of running a script:
./binaries/io vm/_sampleCode/HelloWorld.io
There is no main() function or object that gets executed first in Io. Scripts are executed when compiled.
Running:
./binaries/io
will open the Io interpreter prompt.
You can evaluate code by entering it directly. Example:
Io> "Hello world!" println
==> Hello world!
Statements are evaluated in the context of the Lobby:
Io> print
[printout of lobby contents]
doFile and doString
A script can be run from the interactive mode using the doFile method:
doFile(“scriptName.io”)
The evaluation context of doFile is the receiver, which in this case would be the lobby. To evaluate the script in the context of some other object, simply send the doFile message to it:
someObject doFile(“scriptName.io”)
The doString method can be used to evaluate a string:
Io> doString(“1+1”)
==> 2
And to evaluate a string in the context of a particular object:
someObject doString(“1 + 1”)
Command Line Arguments
Example of printing out command line arguments:
args foreach(k, v, write("'", v, "'\n"))
launchPath
The Lobby "launchPath" slot is set to the location on the initial source file that is executed.
Less is more.
- Ludwig Mies van der Rohe
Io has no keywords or statements. Everything is an expression composed entirely of messages, each of which is a runtime accessible object . The informal BNF description:
exp message arguments symbol terminator |
::= { message | terminator } ::= symbol [arguments] ::= “(“ [exp [ { “,” exp } ]] “)” ::= identifier | number | string ::= “\n” | “;” |
For performance reason s, String and Number literal messages have their results cached in their message objects.
Message arguments are passed as expressions and evaluated by the receiver. Selective evaluation of arguments can be used to implement control flow. Examples:
for(i, 1, 10, i println)
a := if(b == 0, c + 1, d)
In the above code, “ for” and “if” are just normal messages, not special forms or keywords.
Likewise, dynamic evaluation can be used with enumeration without the need to wrap the expression in a block. Examples:
people select(person, person age < 30)
names := people map(person, person name)
There is also some syntax sugar for operators (including assignment), which are handled by an Io macro executed on the expression after it is compiled into a message tree. Some sample source code:
Account := Object clone
Account balance := 0
Account deposit := method(amount,
balance = balance + amount
)
account := Account clone
account deposit(10.00)
account balance println
Like Self[2], Io’s syntax does not distinguish between accessing a slot containing a method from one containing a variable.
An operator is just a message whose name contains no alphanumeric characters (other than ":", "_", '"' or ".") or is one of the following words: or, and, return. Example:
1 + 2
This just gets compiled into the normal message:
1 +(2)
Which is the form you can use if you need to do grouping:
1 +(2 * 4)
Standard operators follow C's precedence order, so:
1 + 2 * 3 + 4
Is parsed as:
1 +(2 *(3)) +(4)
User defined operators (that don't have a standard operator name) are performed left to right.
Io has two assignment messages, “:=” and “=”.
a := 1
which compiles to:
setSlot(“a”, 1)
which creates the slot in the current context. And:
a = 1
which compiles to:
updateSlot(“a”, 1)
which sets the slot if it is found in the lookup path or raises an exception otherwise. By overloading updateSlot and forward in the Locals prototype, self is made implicit in methods.
The following are valid number formats:
123
123.456
0.456
.456
123e-4
123e4
123.456e-7
123.456e2
Hex numbers are also supported (in any casing):
0x0
0x0F
0XeE
Strings can be defined surrounded by a single set of double quotes with escaped quotes (and other escape characters) within.
s := "this is a \"test\".\nThis is only a test."
Or for strings with non-escaped characters and/or spanning many lines, triple quotes can be used.
s := """this is a "test".
This is only a test."""
Comments of the //, /**/ and # style are supported. Examples:
a := b // add a comment to a line
/* comment out a group
a := 1
b := 2
*/
The "#" style is useful for unix scripts:
#!/usr/local/bin/io
That's it! You now know everything there is to know about Io's syntax. Control flow, objects, methods, exceptions are expressed with the syntax and semantics described above.
In all other languages we've considered [Fortran, Algol60, Lisp, APL, Cobol, Pascal], a program consists of passive data-objects on the one hand and the executable program that manipulates these passive objects on the other. Object-oriented programs replace this bipartite structure with a homogeneous one: they consist of a set of data systems, each of which is capable of operating on itself. - David Gelernter and Suresh J Jag
Io’s guiding design principle is simplicity and power through conceptual unification.
concept prototypes messages blocks with assignable scope |
unifies objects, classes, namespaces, locals functions, operators, calls, assignment, variable accesses methods, closures, functions |
In Io, everything is an object ( including the locals storage of a block and the namesp ace itself) and a ll actions are messages ( including assignment). Objects are composed of a list of key/value pairs called slots, and an internal list of objects from which it inherits called protos. A slot’s key is a symbol (a unique immutable sequence) and it’s value can be any type of object.
New objects are made by cloning existing ones. A clone is an empty object that has the parent in it’s list of protos. A new instance’s init slot will be activated which gives the object a chance to initialize itself. Like NewtonScript[3], slots in Io are create-on-write.
me := Person clone
To add an instance variable or method, simply set it:
myDog name := "rover"
myDog sit := method("I'm sitting\n" print)
When an object is cloned, its "init" slot will be called if it has one.
When an object receives a message it looks for a matching slot, if not found, the lookup continues depth first recursively in its protos. Lookup loops are detected (at runtime) and avoided. If the matching slot contains an activatable object, such as a Block or CFunction, it is activated, if it contains any other type of value it returns the value. Io has no globals and the root object in the Io namespace is called the Lobby.
Since there are no classes, there's no difference between a subclass and an instance. Here’s an example of creating a the equivalent of a subclass:
Io> Dog := Object clone
==> Object_0x4a7c0
The above code sets the Lobby slot "Dog" to a clone of the Object object. Notice it only contains a protos list contains a reference to Object. Dog is now essentially a subclass of Object. Instance variables and methods are inherited from the proto. If a slot is set, it creates a new slot in our object instead of changing the proto:
Io> Dog color := "red"
Io> Dog
==> Object_0x4a7c0:
color := "red"
Multiple Inheritance
You can add any number of protos to an object's protos list. When responding to a message, the lookup mechanism does a depth first search of the proto chain.Locals
A method is an anonymous function which, when called, creates an object to store it's locals and sets the local’s proto pointer and it’s self slot to the target of the message. The Object method method() can be used to create methods. Example:
method((2 + 2) print)
An example of using a method in an object:
Dog := Object clone
Dog bark := method("woof!" print)
The above code creates a new "subclass" of object named Dog and adds a bark slot containing a block that prints "woof!". Example of calling this method:
Dog bark
The default return value of a block is the result of the last expression.
Arguments
Methods can also be defined to take arguments. Example:
add := method(a, b, a + b)
The general form is:
method(<arg name 0>, <arg name 1>, ..., <do message>)
A block is the same as a method except it is lexically scoped. That is, variable lookups continue in the context of where the block was created instead of the target of the message which activated the block. A block can be created using the Object method block(). Example of creating a block:
b := block(a, a + b)
Blocks vs. Methods
This is sometimes a source of confusion so it's worth explaining in detail. Both methods and blocks create an object to hold their locals when they are called. The difference is what the "proto" and "self" slots of that locals object are set to. In a method, those slots are set to the target of the message. In a block, they're set to the locals object where the block was created. So a failed variable lookup in a block's locals continue in the locals where it was created. And a failed variable lookup in a method's locals continue in the object to which the message that activated it was sent.
Call and Self Slots
When a locals object is created, it’s self slot is set (to the target of the message, in the case of a method, or to the creation context, in the case of a block) and it’s call slot is set to an object containing the following slots:
slot sender message activated slotContext target |
references locals object of caller message used to call this method/block the activated method/block context in which slot was found current object |
Variable Arguments
The “call message” slot in locals can be used to access the unevaluated argument messages. Example of implementing if() within Io:
if := method(
(call sender doMessage(call message argAt(0))) ifTrue(
call sender doMessage(call message argAt(1))) ifFalse(
call sender doMessage(call message argAt(2)))
)
myif(foo == bar, write("true\n"), write("false\n"))
The doMessage() method evaluates the argument in the context of the receiver.
A shorter way to express this is to use the evalArgAt() method on the call object:
if := method(
call evalArgAt(0) ifTrue(
call evalArgAt( 1 ) ) ifFalse(
call evalArgAt( 2 ) )
)
myif(foo == bar, write("true\n"), write("false\n"))
If an object doesn't respond to a message, it will invoke its "forward" method if it has one. Here’s an example of how to print the information related lookup that failed:
MyObject forward := method(
write("sender = ", call sender, "\n")
write("message name = ", call message name, "\n")
args := call message argsEvaluatedIn(call sender)
args foreach(i, v, write("arg", i, " = ", v, "\n") )
)
Sends the current message to the receiver's proto with the context of self. Example:
A := Object clone
A m := method(write("in A\n"))
B := A clone
B m := method(write("in B\n"); resend)
B m
will print:
in B
in A
For sending other messages to the receiver's proto, super is used.
Sometimes it's necessary to send a message directly to a proto. Example:
Dog := Object clone
Dog bark := method(writeln("woof!"))
fido := Dog clone
fido bark := method(
writeln("ruf!")
super(bark)
)
Both resend and super are implemented in Io.
getSlot
T he "getSlot" method can be used to get the value of a block in a slot without activating it:
myMethod := Dog getSlot("bark")
Above, we've set the locals object’s "myMethod" slot to the bark method. It's important to remember that if you then want use the myMethod without activating it, you'll need to use the getSlot method:
otherObject newMethod := getSlot("myMethod")
Here, the target of the getSlot method is the locals object.
code
The arguments and expressions of methods are open to introspection. A useful convenience method is “code”, which returns a string representation of the source code of the method in a normalized form.
Io> method(a, a * 2) code
==> “method(a, a *(2))”
Io has predefined singletons for true, false and nil. true and false are used for boolean truth values and nil is typically used to indicate an unset or missing or unavailable value.
The standard comparison operations (==, !=, >=, <=, >, <) return either the true or false.
Io> 1 < 2
==> true
if
The Lobby contains the condition and loop methods. A condition looks like:
if(<condition>, <do message>, <else do message>)
Example:
if(a == 10, "a is 10" print)
The else argument is optional. The condition is considered false if the condition expression evaluates to false or nil, and is considered true otherwise.
The result of the evaluated message is returned, so:
if(y < 10, x := y, x := 0)
is the same as:
x := if(y < 10, y, 0)
Conditions can also be used in this form (though not as efficiently):
if(y < 10) then(x := y) else(x := 2)
Else-if is supported:
if(y < 10) then(x := y) elseif(y == 11) then(x := 0) else(x := 2)
As well as Smalltalk style ifTrue, ifFalse, ifNil and ifNonNil methods:
(y < 10) ifTrue(x := y) ifFalse(x := 2)
Notice that the condition expression must have parenthesis surrounding it.
loop
The loop method can be used for “infinite” loops:
loop(“foo” println)
while
Like conditions, loops are just messages. while() takes the arguments:
while(<condition>, <do message>)
Example:
a := 1
while(a < 10,
a print
a = a + 1
)
for
for() takes the arguments:
for(<counter>, <start>, <end>, <do message>)
The start and end messages are only evaluated once, when the loop starts.
Example:
for(a, 0, 10,
a print
)
To reverse the order of the loop, just reverse the start and end values:
for(a, 10, 0, a print)
Note: the first value will be the first value of the loop variable and the last will be the last value on the final pass through the loop. So a loop of 1 to 10 will loop 10 times and a loop of 0 to 10 will loop 11 times.
Example of using a block in a loop:
test := method(v, v print)
for(i, 1, 10, test(i))
repeat
The Number repeat method is simpler and more efficient when a counter isn’t needed.
3 repeat(“foo” print)
==> foofoofoo
break and continue
The flow control operations break and continue are supported in loops. For example:
for(i, 1, 10,
if(i == 3, continue)
if(i == 7, break)
i print
)
Would print:
12456
return
Any part of a block can return immediately using the return method. Example:
Io> test := method(123 print; return "abc"; 456 print)
Io> test
123
==> abc
Io uses coroutines ( user level cooperative threads) , instead of preemptive OS level threads to implement currency. This avoids the substantial costs (memory, system calls, locking, caching issues, etc) associated with native threads and allows Io to support a very high level of concurrency with thousands of active threads.
The Scheduler object is responsible for resuming coroutines that are yielding. The current scheduling system uses a simple first-in-last-out policy with no priorities.
An actor is an object with it's own thread (in our case, it’s own coroutine) which it uses to process it's queue of asynchronous messages. Any object in Io can be sent an asynchronous message by placing a @ or @@ before the message name. (think of the "a" in @ as standing for "asynchronous")
Example:
result := self foo
// synchronous message
futureResult := self @foo // async message, immediately return a Future
self @@foo // async message, immediately return nil
When an object receives an asynchronous message it puts the message in its queue and, if it doesn't already have one, starts a coroutine to process the messages in its queue. Queued messages are processed sequentially in a first-in-first-out order. Control can be yielded to other coroutines by calling "yield". Example:
obj1 := Object clone
obj1 test := method(for(n, 1, 3, n print; yield))
obj2 := obj1 clone
obj1 @@test; obj2 @@test
while(Scheduler activeActorCount > 1, yield)
This would print "112233".
Here's a more real world example:
HttpServer handleRequest := method(aSocket,
HttpRequestHandler clone @@handleRequest(aSocket)
)
An object will automatically yield between processing each of its asynchronous messages. The yield method only needs to be called if a yield is required during an asynchronous message execution.
It's also possible to pause and resume an object. See the concurrency methods of the Object primitive for details and related methods.
Io's futures are transparent. That is, when the result is ready, they become the result. If a message is sent to a future (besides the two methods it implements), it waits until it turns into the result before processing the message. Transparent futures are powerful because they allow programs minimize blocking while also freeing the programmer from managing the fine details of synchronization.
Auto Deadlock Detection
An advantage of using futures is that when a future requires a wait, it will check to see if pausing to wait for the result would cause a deadlock and if so, avoid the deadlock and raise an exception. I t performs this check by travers ing the list of connected future s .
The @ and @@ Operators
The @ or @@ before an asynchronous message is just a normal operator message. So:
self @test
Gets parsed as(and can be written as):
self @(test)
An exception can be raised by calling raise() on an exception proto.
exceptionProto raise(<description>)
There are three predefined children of the Exception proto: Error, Warning and Notification. Examples:
Exception raise("generic foo exception")
Warning raise("No defaults found, creating them")
Error raise("Not enough memory")
To catch an exception, the try() method of the Object proto is used. try() will catch any exceptions that occur within it and return the caught exception or nil if no exception is caught.
e := try(<doMessage>)
To catch a particular exception, the Exception catch() method can be used. Example:
e := try(
// ...
)
e catch(Exception,
writeln(e coroutine backtraceString)
)
The first argument to catch indicates which types of exceptions will be caught. catch() returns the exception if it doesn't match and nil if it does.
To re-raise an exception caught by try(), use the pass method. This is useful to pass the exception up to the next outer exception handler, usually after all catches failed to match the type of the current exception:
e := try(
// ...
)
e catch(Error,
// ...
) catch(Exception,
// ...
) pass
Custom exception types can be implemented by simply cloning an existing Exception type:
MyErrorType := Error clone
This document is not meant as a reference manual, but an overview of the base primitives and an bindings is provided here to give the user a feel for what is available and where to look in the reference documentation for further details.
Primitives
Primitives are objects built into Io whose methods are implemented in C and (except for the Object primitive) store some hidden data in their instances. For example, the Number primitive has a double precision floating point number as it's hidden data. All Io primitives inherit from the Object prototype and are mutable. That is, their methods can be changed. The reference docs contain more info on primitives.
The ? Operator
Sometimes it's desirable to conditionally call a method only if it exists (to avoid raising an exception). Example:
if(obj getSlot("foo"), obj foo)
Putting a "?" before a message has the same effect:
obj ?foo
1 2 3 4 5 6 |
Goldberg, A et al. Smalltalk-80: The Language and Its Implementation Addison-Wesley, 1983 Ungar, D and Smith, RB. Self: The Power of Simplicity OOPSLA, 1987 Smith, W. Class-based NewtonScript Programming PIE Developers magazine, Jan 1994 Lieberman H. Concurrent Object-Oriented Programming in Act 1 MIT AI Lab, 1987 McCarthy, J et al. LISP I programmer's manual MIT Press, 1960 Ierusalimschy, R, et al. Lua: an extensible extension language John Wiley & Sons, 1996 |