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:
In LISP it looks like
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:
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:
And I could chain them together:
But in LISP, it would be:
(method object arg)
(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:
is equivalent to:
(lookup object ‘(member))
And so a more bearable interface to the system is available. That makes message-passing look like this:
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:
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)))
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.