Message-passing Objects in Scheme

OK, so there’s this fun thing called object-oriented programming. Scheme is a (mostly) functional programming language, but objects can be put in there, and as a LISP, adding syntactic niceties to support this feature shouldn’t be too difficult. Whilst semi-sold on the idea of OOP as a language paradigm (doing so in my case requires a very loose definition of language), I think it is more important as an issue of design. OOP makes it easier to design certain kinds of large systems. This is less obvious when the language does not make such abstractions easy (GTK anyone?).

Now, I’m certainly not the first to try this idea. Common Lisp has CLOS, which is based upon many other attempts at this concept, and there are versions of CLOS for Scheme. CLOS is both powerful and flexible, and it manages to gel well with the rest of the language. So, why would I want to re-implement it?

Well, I don’t like the way selectors work. This is in part because of the “fitting nicely into the rest of the language” thing. In order to access a member of an object in any Simula-derived language, you do something like:

object.member

In LISP it looks like

(member object)

There isn’t a huge amount of difference, syntactic issues aside. However, the first describes the situation in a better way: it imparts more semantic information. I can see this decision is based upon the way LISP encapsulates abstract data. That is, a data type is defined by the operations which can be performed on it. This works well when we’re just talking about data, but objects aren’t just data.

Objects describe behaviour as well as state. When you send a message to an object, it does something with its data in a well-defined way. So there should be a way of more closely associating methods to objects. CLOS gets around this by ignoring the issue. It does so with multi-methods, which are cool, but aren’t message-passing. A neat hack is still just a hack.

This reason doesn’t sound very pragmatic. I could argue that in large systems it certainly is pragmatic to be able to group related concepts together closely, but I’ll try a different tack. Let’s say you wanted to find the member of a member of an object. Again, in Simula:

object.member1.member2

And in LISP:

(member2 (member1 object))

Starting to seem less practical, what?

Now let’s return to the whole method thing. In CLOS, multi-methods and objects are related, but the binding is rather weak. If I were going to call a method on an object in Simula, I’d do something like:

object.method(arg)

And I could chain them together:

object.method1(arg1).method2(arg2)

But in LISP, it would be:

(method object arg)

and

(method2 (method1 object arg1) arg2)

Which is starting to get silly. Add in selectors, from above, and a single line of Simula is now a nested parenthesis hellhole.

Message-passing has been tried in LISP, and it was a failure because it didn’t work with higher-order procedures. That is:

(send object ‘method arg)

Would need to be wrapped in a lambda if it is to be used with a map or what have you. But this was Common Lisp. Scheme is a 1-LISP, in that procedures and variables occupy the same namespace (which I feel is a more reasonable approach in a language where code and data are literally the same thing). This means that you don’t need to do any escaping for a named procedure to behave like a procedure. That is:

((lambda (x) x) x)

Will return x in Scheme, but to get the same result in Common Lisp, you’d have to do:

(funcall #'(lambda (x) x) x)

So, in Scheme, methods can be treated simply as members of objects, so:

(lookup object ‘(member))

Can return some data on the object, and:

((lookup object ‘(method)) arg)

Will call that method.

To get members of members you could do something like:

(lookup object ‘(member1 member2))

Of course, you can replace that lookup with a reader macro, so that, for instance:

#$object.member

is equivalent to:

(lookup object ‘(member))

And so a more bearable interface to the system is available. That makes message-passing look like this:

(#$object.method arg)

Which is lovely.

Another thing this abstraction avails to the programmer, which is part of more closely tying the object to its members, is that a method can exist within the local scope of the object in which it is defined. That means that you don’t need to give the object as an argument to the method, and you don’t need to worry about what accessors an object has. You can just use the data on self directly. This makes it easier to reason about how objects are going to behave, as well as easily enabling higher-order procedures as a means of combination:

(map #$object.method collection)

Works as expected.

This system does not, however, account at all for the chaining I mentioned above:

object.method1(arg1).method2(arg2)

is very difficult to do with just the #$ syntax. You could achieve it with:

((lookup (lookup object ‘(method1) arg1) ‘(method2)) arg 2)

or, using #$:

(let ((res (#$object.method1 arg1)))
(#$res.method2 arg2))

But, in either case, that looks even worse than the CLOS method, and worse, forces a leak on our lovely #$ abstraction. However, because we’re using macros, we can try processing the syntax to obviate the need for this hack. Simply deciding that:

#$(object.method1 arg1 .method2 arg2)

is the same as the example given above, sorts out some of our worries. Of course, the decision about whether to call or return a method from this statement is complicated when that method takes no parameters. In that case, the easiest way to handle it is for the system to always return a method if it is not given any parameters. This way, the programmer (who is presumably familiar with the object’s interface) can signal that they wish that method to be invoked by wrapping the thing in parentheses:

(#$(object.method1 arg1 .method2 .arg2))

will call whatever method2 returned, passing no parameters.

I have an implementation that does all I’ve said above, working in Guile, although I’m not quite happy with it. It’s about 100 lines (71 opening parentheses), and although it contains elements unrelated to the primary focus of this post, I think it could be a great deal simpler. This discussion also leaves out important issues in object-oriented design, such as data-hiding, polymorphism and inheritance, which is something my object system leaves out as well, at present. I am working on rectifying both situations, and I shall post my results here, along with the final code.

Yeah, that’s the typical pro-CLOS viewpoint. I’m aware of it. You have functions that are polymorphic, and there’s some type-dispatch sugar on some special structures that go in.

Polymorphism is an important part of object-oriented programming, but it’s not the only part. There’s the whole “objects encapsulate state and behaviour” thing, which the chap there describes as “sheer lunacy,” but is actually part of the point. It makes it easier to reason about certain types of large systems to have entities that pass messages to one another.

Like I said, having functions that do multiple dispatch is a powerful tool, but to call it “object oriented” is rather misleading, in my humble opinion, as it separates code from data, state from behaviour etc. etc.

I have absolutely no objection to polymorphic functions existing, alongside object systems. To be honest, I think that polymorphic functions would have been more useful in generalising operations on ordinary data, which was left out (for good reason, so I hear, although I dunno what that reason is).