![]() |
Simplicity is the ultimate sophistication.
- Leonardo da Vinci
Io is small prototype-based programming language. The ideas in Io are mostly inspired by Smalltalk (all values are objects), Self, NewtonScript and Act1 (prototype-based differential inheritance, actors and futures for concurrency), LISP (code is a runtime inspectable/modifiable tree) and Lua (small, embeddable).
Io is commercial software friendly, open sourced under a BSD license and the standard bindings give a strong preference to libraries with BSD/MIT style licenses.
Features
What's Different
A language that is small, simple, multi-platform and well suited for embedded use.
Uses
The target uses are portable desktop application development, internet server applications and embedded scripting.
It Just Works
The goal for the feel of Io is to be what might be called the Apple of programming languages. That is, things should "Just Work". For example, one shouldn't have to be a system administrator to install it or need to set environment variables to use it. One should be able to drop the executable somewhere and use it without hassles. An Io desktop application should work without an installer and without having to be placed (or to stay) at a particular file path.
Bindings are Good
The Smalltalk/LISP/Java communities generally view any code outside the VM as "unpure" or bad. Io instead embraces the idea of using C bindings for advanced functionallity and performance sensitive features (graphics, sound, encryption, array processing, etc). It does this while maintaining multi-platform support by encouraging the use of platform independent or multi-platform C libraries (OpenGL, PortAudio, etc).
Objects are Good
When possible, bindings should provide an object oriented interface and not simply mimic low-level C APIs (as Python often does). Also, concrete design is favored over the abstract - you shouldn't be required to use a dozen classes to do a simple operation (as Java often does).
IDE
Eventually, I'd like to see Io have an interactive visual programming environment. Something similar to Self, but with visually structured editing down to the method source code level.
In the top level folder of the release:
makeShould build the io and place any binaries in the binaries subfolder. If you encounter a problem, please post it to the Io mailing list and we'll try to help solve it.
Io doesn't need to be put in a particular directory or to have any environment variables set. But if you like, you can have install itself using:
make installTesting
In the vm folder running:
make testwill run a set of unit tests to make sure it is working.
There are some example scripts in the vm/_tests/_sampleCode/ directory. You can run them from the command line like this:
./io _tests/examples/HelloWorld.io
Running:
./iowith no arguments will open the Io interpreter prompt. You can evaluate code by entering it directly. Example:
Io> writeln("Hello world!") Hello world!Statements are evaluated in the context of the Lobby:
Io> print [printout of lobby contents]
Command line arguments after the file name are put into a List object and stored in the Lobby's "args" slot. Here's an example of one way to print them out:
Lobby args foreach(k, v, write("'", v, "'\n"))
There is no main() function or object that gets executed first in Io. Scripts are executed when compiled.
Also, the Lobby slot "launchPath" is set to the location on the initial source file that is executed.
Objects in Io contain slots, which are key/value pair dictionaries and a list of protos, which are objects which it inherits from. A slot's key is a string and its value can be any type of object. Methods are simply slots whose value is set to a method/block or a CFunction primitive. Instance and/or "class" variables are slots with other types of values. The root object in the Io namespace is called the Lobby.
Less is more.
- Ludwig Mies van der Rohe
Other than comments, strings and numbers, everything in Io is a message in one of 3 forms (normal, operator or assignment). The normal form is:
<message name> ( <arg1>, <arg2>, ...) <attached message, new line or semicolon>
The parentheses are optional if there are no arguments. The semicolon
is optional. If a semicolon or a return is present, it ends the message chain.
If not, it means: "use the result on the left as the target of the message on the right."
Examples:
Dog fetch(stick) color set(.5, 1, 1, alpha / 2) peopleOver20 := people select(p, p age > 20) sort(p, p age)Every statement is an expression in Io - that is, every statement returns a value.
Operators
The other message syntax is for operators. An operator is just a method whose name contains no alphanumeric characters(other than ":", "_", '"' or ".") or is one of the following words: or, and, return.
<operator message name> <argument> <another operator, new line or semicolon>
Example:
1 + 2This 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 + 4Is parsed as:
1 +(2 *(3)) +(4)User defined operators (that don't have a standard operator name) are performed left to right.
Assignment
In Io, := means "setSlot", which will create the slot if needed. And = means "updateSlot" , which will update the slot if present in the inheritance path and raise an exception otherwise. Note that the update occurs in the object which receives the updateSlot message, which may not be the object in the inheritance path which contains the slot.
An assignment is just an operator message that gets compiled into the appropriate setSlot() call. Example:
Dog := Object clonegets compiled to:
setSlot("Dog", Object clone)Here, the "setSlot" message gets sent to the locals object which is the default target. Likewise:
Dog = Object clonegets compiled to:
updateSlot("Dog", Object clone)
Numbers
The following are valid number formats:
123 123.456 0.456 .456 123e-2 123e2 123.456e-2 123.456e2Hex numbers are also supported (in any casing):
0x0 0x0F 0XeE
Strings
Strings can be defined surrounded by a single set of double quotes with escaped quotes(and other escape charaters) within. For strings with no escaped characters and/or spanning many lines, triple double quotes can be used. Examples:
s := "this is a \"test\".\nThis is only a test." s := """this is a "test". This is only a test."""
Comments
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 all use the same syntax and semantics described above.
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 true if the condition message returns a non-nil value and false 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 also supported:
if(y < 10) then (x := y) elseif (y == 11) then (x := 0) else (x := 2)
Loops
Like conditions, loops are just messages. while() takes the arguments:
while(<condition>, <do message>)Example:
while(a < 10, a print a = a + 1 )for() takes the arguments:
for(<counter>, <start>, <end>, <do message>)The start and end messages are only evaluated once, when the loop starts.
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))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
Instantiation
To create a new instance of an object, just call its "clone" method. Example:
me := Person cloneTo 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.
Inheritance
Since there are no classes, there's no difference between a subclass and an instance. Example of creating a "subclass":
Io> Dog := Object clone Io> Dog Object_0x4a7c0 protos(Object_0x4a540)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 protos(Object_0x4a540) 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.
Forwarding
If an object doesn't respond to a message, it will invoke its "forward" method if it has one. You can get the name of the message and it's arguments from the Message object in the "thisMessage" slot.
MyObject forward := method( write("sender = ", sender, "\n") write("message name = ", thisMessage name, "\n") args := thisMessage argsEvaluatedIn(sender) args foreach(i, v, write("arg", i, " = ", v, "\n") ) )
resend
Sends the current message (thisMethod) 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 mwill print:
in B in AFor sending other messages to the receiver's proto, super is used.
super
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 )
Locals
The first message in a block/method has its target set to the block locals. The locals object has its "proto" and "self" slots set to the target object of the message. So slot lookups look in the locals first, then get passed to the target object and then to its proto, etc until reaching the Lobby. In this way, an assignment with no target goes to the locals first:
a := 10creates an "a" slot in the locals object and sets its value to 10. To set a slot in the target, get its handle by referencing the "self" slot of the locals first:
self a := 10sets the "a" slot in the target.
Introspection
To get the value of a block in a slot without activating it, use the "getSlot" method:
myMethod := Dog getSlot("bark")Above, we've set the locals object "myMethod" slot to the bark method. It's important to remember that if you then want to pass the method value without activating it, you'll have to use the getSlot method:
otherObject newMethod := getSlot("myMethod")Note: Here, the target of the getSlot method is the locals object.
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
A method is an anonymous function which, when called creates an Object to store it's locals and sets it's proto and self slots to the target of the message. The Lobby method method() can be used to create methods. Example:
method(2 + 2 print)Using a method in an object example:
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 return value of its last message.
Blocks
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:
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.
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>)Variable Arguments
The thisMessage slot that is preset (see next section) in locals can be used to access the unevaluated argument messages. Example:
myif := method( if (sender doMessage(thisMessage argAt(0)), sender doMessage(thisMessage argAt(1)), sender doMessage(thisMessage argAt(2))) ) myif(foo == bar, write("true\n"), write("false\n"))The doMessage() method evaluates the argument in the context of the receiver.
Preset Locals
The following local variables are set when a method is activated:
|
When a block is activated, the same locals are set, but the self slot is set to the following:
|
Returns
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
The "activate" slot
Normally, when a slot holding a non-CFunction or Block object is accessed, it returns the object itself. However, by calling the setIsActivatable() method, we can change this behavior to call the object's activate slot instead.
inc := Object clone inc count := 1 inc activate := method(v, count = count + v) inc setIsActivatable(1) inc(2) // returns 3
exceptionProto raise(<name>, <description>)There are three predefined children of the Exception proto: Error, Warning and Notification. Examples:
Exception raise("foo", "generic foo exception") Error raise("Simulation", "Not enough memory") Warning raise("Defaults", "No defaults found, creating them")To catch an exception, the try() method of the Object proto is used. try() will catch any exceptions that occur within it. If one is caught, "try" returns it, otherwise it returns Nop (an object that returns itself to any message):
try(<doMessage>)To catch a particular exception, the Exception catch() method can be used. Example:
try( // code ) catch(Exception, e, write("caught ", e name, ":", e description, "\n") )The first argument to catch indicates which types of exceptions will be caught, the second specifies the local variable which is assigned the value of the returned exception.
catch() returns Nop if it matched the type i.e. it caught the exception. Otherwise catch() returns the exception. This allows for chaining catches:
try( // code ) catch(Warning, e, // code ) catch(Error, e, write("caught ", e name, ":", e description, "\n") )When a catch() caught the exception, following catches will be sent to Nop and therefore be ignored.
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:
try( // code ) catch(Error, e, // code ) passTo implement your own exception types, simply clone Exception:
MyErrorType := Error clone
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.
result := self foo // normal, synchronous message send futureResult := self @foo // asynchronous message send, immediately return a Future self @@foo // asynchronous message send, immediately return NilWhen an object receives an asynchronous message it puts the message in its queue and, if it doesn't already have one, starts a light weight thread (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, handler := HttpRequestHandler clone handler @@handleRequest(aSocket) )Yielding
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.
Transparent Futures
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 powerfull 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(it traverses the list of connected futures to do this). An "Io.Future.value" exception is raised if the action would cause a deadlock.
Garbage Collection
Even if an object is unreferenced, it will not be garbage collected until its message queue is processed.
The @ and @@ Operators
The @ or @@ before an asynchronous message is just a normal operator message. So:
self @testGets parsed as(and can be written as):
self @(test)
Efficiency
Since Io uses coroutines for concurrency (which are small and fast), a very large number (10s of thousands) of actors can be handled without significant performance issues. Also, the waits described above are not busy-waits. The coroutine that is waiting is actually removed from the scheduler until the result it is waiting on is ready.
Platform Dependence
The coroutine code contains a few lines of platform specific code but is supported on a number of platforms (see the getting started section for a list of supported platforms).
#include "IoState.h" typedef struct { IoState *state; } Controller; Controller *Controller_new(void) { Controller *self = calloc(1, sizeof(Controller)); self->state = IoState_new(); IoState_callbackContext_(state, self); IoState_printCallback_(state, (IoStatePrintCallback *) Controller_print_); IoState_exceptionCallback_(state, (IoStateExceptionCallback *) Controller_exception_); IoState_printCallback_(state, (IoStateExitCallback *) Controller_exit); return self; } /* --- callback methods --- */ void Controller_print_(void *self, char *s) { fputs(s, stdio); } void Controller_exception_(void * self, IoException *e) { printf("Exception: %s - %s\n", IoException_name(e), IoException_description(e)); } void Controller_exit(void *self) { exit(0); } /* --- main --- */ void Controller_main(Controller *self, int argc, const char *argv[]) { IoObject *result; IoState_argc_argv_(self->state, argc, argv); result = IoState_doCString_(self->state, "1+2"); IoObject_print(result); } /* --- main.c --- */ #include "Controller.h" int main(int argc, const char *argv[]) { Controller *c = Controller_new(); Controller_main(c, argc, argv); Controller_free(state); return 0; }
Writing your own primitives and binding C libraries
Binding a C library and adding a new primitive are the same thing in Io. All primitives work in the same way. Take a look at the source of any of the built-in Primitives such as IoList.h and IoList.c. Here's an example of an implementation of a method in IoList.c
IoObject *IoList_atInsert(IoList *self, IoObject *locals, IoMessage *m) { int index = IoMessage_locals_intArgAt_(m, locals, 0); IoObject *v = IoMessage_locals_valueArgAt_(m, locals, 1); List_at_insert_(LISTIVAR(self), index, IOREF(v)); return v; }Further docs on this forthcoming...
Io should be generally compatible with UTF-8 unicode (which is backward compatible with ASCII) but not all of the string access and manipulation methods will work as expected with it.
Recursion
Recursion works as expected in Io. Example:
factorial := method(n, if (n==1, return 1) return n * factorial(n - 1) )Like C, the depth is limited by stack space. The following implementation would be much faster and not limited by stack space:
factorial := method(n, v := 1 for(i, 1, n, v = v * i) return v )
Licensing
All the Io VM code itself is BSD licensed so it's basically ok for any use (including commercial use) as long as you include the copyright licenses - see the _BSDLicence.txt file for details). Io uses a number of libraries that others have written though. The libraries used in IoVM and IoServer are all (as far as I'm aware) also BSD licensed - so you just need to include their copyrights. IoDesktop, however, uses several LGPL libraries. As far as I know, you should be able to avoid open sourcing an app that embeds IoDesktop by dynamically linking it though.
Please see the ReleastNotes.txt file for individual contribution notes as the contributors list has grown too long to list here. I'll just list some major contributors:
Also thanks to: