The Io language is a small, very cool object-oriented language.
There's a broad consensus among the programming-language aficionados I talk to: object-oriented programming is not nearly as useful as it was once considered to be. It's easy to come to that conclusion after seeing the OOP-mania of the 90's give way to bloated, complicated codebases, the infamous piles of
AbstractStrategyFactorysubclasses and dependency injection frameworks needed to build flexible Java software, the signed-in-triplicate verbosity of Objective-C code, or the impossible-to-optimize amounts of indirection in Ruby or Python.
Certainly, some problem domains map well to object-oriented programming: the traditional example is GUI programming, in which the little heterogeneous chunks of state that are objects map quite well to the widgets in a traditional WIMP interface. But in other areas, the object-oriented approach falls flat: high-performance video games, for example, have realized that traditional object-oriented modeling techniques result in abysmal cache performance, and end up using object-oriented languages to produce very much non-object-oriented designs.
It's common to see fans of object-orientation object, "Ah, well, that's because C++ isn't really what we mean," which does sound a little bit weaselly, No true object puts sugar on its porridge! but even Alan Kay, originator of the phrase "object-oriented", once said In particular, in his awesome talk at OOPSLA in 1997, which you should certainly watch at some point.
I made up the term 'object-oriented', and I can tell you I didn't have C++ in mind!
Well, what did Kay have in mind? From a classic response by Kay himself:
OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things. It can be done in Smalltalk and in LISP. There are possibly other systems in which this is possible, but I'm not aware of them.
Everything feature Kay describes is something that forces the programmer to write abstract programs: hiding of local state means that features must rely on external interfaces; messaging means that interfaces must be abstract and generally specified; late-binding means the programmer must write code that works with alternate indistinguishable implementations, rather than specifying a concrete implementation up-front. Clearly, C++ does not fit the bill: it lacks messaging as a language feature, has only optional protection of state-process, and heavily discourages late-binding when it allows it at all! So: what does a Kay-style object-oriented language look like?
Kay wrote the above passage in 2003, and since then, at least a few other languages have come around that are in the original spirit of Kay's vision. One of them is Steve Dekorte's Io language, which I'd like to provide a whirlwind tour of here.
The wonderful thing about Io is how small it is: it chooses a few simple pieces, adds some simple sugar, and builds everything else out of those pieces. It's very much feels like the distilled essence of a language paradigm.
"Hello, world!" println
Syntactically, it's very sparse—the above snippet, which contains just two tokens, invokes a method called
printlnon a string literal. More properly, we're sending a message. The difference seems pedantic at first, but Io—in contrast to Java or C++—lets us consider the message as a thing, examining it as a value or sending it elsewhere by delegating or duplicating it. We could include the trailing parentheses—as the
printlnmethod here takes an empty argument list—but Io allows us to omit them.
"Hello, world!" println()
Unlike most members of the SmallTalk family, Io doesn't have unusual split-apart method names. In SmallTalk, method names have "holes" for arguments, indicated by a colon. A method name will look like
withFoo:andBar:and invoking it will look like
myObj withFoo: y andBar: z. This is a compelling design choice in that it often forces a programmer to document an interface, because a method will look like
rectWithWidth: x andHeight: y, giving you omnipresent documentation about the order of arguments. It is, however, an unusual design choice. A method name is a single identifier, and it takes trailing arguments in parens, unless there are no arguments. There's some special syntax for operators, to make precedence work right, but operators are themselves sugar for calling methods named things like
# we can write this 2 + 3 * 4 println # or, equivalently, this 2 +(3 *(4)) println()
It's an imperative language, so we can create and assign variables:
Io> x := 2 Io> x println 2 Io> x = x + 1 Io> x println 3
But assignment is also syntactic sugar—underneath, it's still method calls! The code above can be desugared to explicit calls to assignment-related methods:
Io> setSlot("x", 2) Io> getSlot("x") println() 2 Io> updateSlot("x", x +(1)) Io> getSlot("x") println() 3
If you look closely you'll notice that those methods aren't being called on any object in particular: when we don't supply an explicit object to call a method on, it will get called on some ambient
selfobject, sort of like
thisin other OO languages. When we're sitting at the interactive language prompt, that ambient object is called the
Lobby. The above code is therefore also equivalent to Although notice that
Lobbyis itself a variable, so it could itself be taken as sugar for
getSlot("Lobby")called on the
Lobbyobject. This starts to hint at the stack of turtles underneath the Io language: it really is objects all the way down, in several ways.
Io> Lobby setSlot("x", 2) Io> Lobby getSlot("x") println() 2 Io> Lobby updateSlot("x", x +(1)) Io> Lobby getSlot("x") println() 3
So in Io, all actions are method calls, even "primitive" actions like assignment or variable access.
clonemethod, which we can use on the generic built-in
Io> myPoint := Object clone
Once we have a new object, we can use
:=to change the values of its slots, or local values.
Io> myPoint x := 2 Io> myPoint y := 8 Io> (myPoint x + myPoint y) println 10
If we clone an object, the new object remembers which object it was cloned from—its prototype—and every time we look up a slot in that object, it will first check whether it has that slot; otherwise, it'll check to see if its prototype has the slot, and so on back up the chain. That means we can clone
myPointinto a new object, but the new object will still have access to everything we defined on
Io> newPoint := myPoint clone Io> newPoint x println 2
We can override values on
newPointwithout changing them on its parent object:
Io> newPoint x := 7 Io> newPoint x println # The child has the new value 7 Io> myPoint x println # The parent still has the old one 2
On the other hand, changes to the parent object will be reflected in non-overridden values on the child object:
Io> myPoint y = 3 # we can change the parent value Io> newPoint y println # and the child can see it 3
We can create methods on objects as well, using slot assignment and the
methodconstructor. Within the code of a
method, the ambient object points to the object in which the method is held, so all variables accesses will be looked up inside the object that holds the method:
Io> myPoint isOrigin := method(x == 0 and y == 0) Io> myPoint isOrigin println false
This means that if we copy that method to another object, the
yvariables referenced in the method will now refer to that new object. Determining what
selfmeans for a method like this is simple: it refers to the object through which we invoke the method.
Io> otherPoint := Object clone Io> otherPoint isOrigin := myPoint getSlot("isOrigin") Io> otherPoint x := 0 Io> otherPoint y := 0 Io> otherPoint isOrigin println true
Methods can of course take arguments:
Io> myPoint eq := method(other, x == other x and y == other y) Io> myPoint eq(myPoint) println true Io> myPoint eq(otherPoint) println false
Because we can clone any object, any object can serve as prototype for another object. I probably would, in practice, build up a proper Point abstraction a little bit differently:
Point := Object clone Point new := method(nx, ny, p := Point clone; p x := nx; p y := ny; p) Point isOrigin := method(x == 0 and y == 0) Point add := method(other, new (x + other x, y + other y)) Point sub := method(other, new (x - other x, y - other y))
Here I fill in all the relevant methods on a
Point, and when I want to create an "instance", I clone the object and fill in the
yvalues. Cloning doesn't just serve the same purpose as instatiation in other OO languages, though; it's also how we'd implement subclassing. To create a new "subclass" of
Point, I clone the
Pointobject and start filling in new methods instead of instantiating variables:
MutablePoint := Point clone MutablePoint setX := method(nx, x = nx; self) MutablePoint setY := method(ny, y = ny; self)
For that matter, there's no reason we have to distinguish between instantiating and subclassing: that's just me explaining things in the traditional terms of class-based OO languages. We could simultaneously create a new "instance" and add extra methods, which corresponds neither strictly to subclassing nor to instantiation. If that object turns out to be useful, we can create new copies by cloning and modifying those as needed, allowing that "instance" to form a new "class". It's really quite flexible, and extensive resources have been written about how to use prototype-based modelling effectively.
So we know that
methods look up their locals in the object where they're stored. But consider the classic Scheme counter, a function which returns one number higher every time you call it:
(define (mk-counter) (let ((n 0)) (lambda () (set! n (+ n 1)) n)))
If we try to translate this to Io using a
method, we'll run into a problem:
mkCounter := method( n := 0 method( n = n + 1 ) )
When we run this, we get an error:
Io> c := mkCounter Io> c Exception: Object does not respond to 'n'
I said before: a
methodlooks up any variables mentioned inside the context where it's stored. The inner
methodwe create and return is stored in the
Lobby, because we're calling this at the prompt. Therefore, it is looking for
Lobby, and not in the enclosing lexical scope! If we add an
Lobby, then our code will start working:
Io> n := 0 Io> c println 1 Io> c println 2
But that's not what we wanted! We want the variable to be hidden inside a closure, so we have private, exclusive access to it. So in this case, instead of using a
method, we can use a
block, which is like a
methodexcept that variables are looked up in the enclosing lexical scope intead.
mkCounter := method( n := 0 block( n = n + 1 ) )
blocks have to be invoked with a
Io> c := mkCounter Io> c call 1 Io> c call 2
blocks, the place where local variables are stored is just an object: they have a new fresh object to store new local variables, but the prototype of that object that corresponds to the location of the
methodor the enclosing static scope around the
block. Looking up variables in those scopes uses the same
getSlot(...)operation to look up the prototype chain, and assigning to a slot uses the same
updateSlot(...)operations. It really is objects (and messages) all the way down.
At this point, I've explained almost all the core features of the Io language. There are some more dynamic features: an object can, for example, resend or forward messages to other objects, and Io has frankly staggering amounts of introspection. Methods and blocks (which are, themselves, objects) even have their own
codemethod, which gives us the source code of the method in a manipulable form at runtime, so we can introspect on (and modify) the AST itself. Additionally, Io has a well-designed standard library, a nice concurrency model (both actors and futures implemented via coroutines) and a clean, well-defined C interface, making it incredibly easy to embed into a larger project as a scripting language.
But a major reason I like Io is that it builds so much of itself on top of so few, straightforward features: a barebones grammar, combined with cloning objects and dispatching messages gives us a lot of expressive power. This is much closer, language-wise, to what Kay had in mind: late-bound, message-passing-based interfaces that hide internal state behind public APIs, and very little else.