Lisp is much older than object oriented programming. When OOP got popular people wanted to do OOP in Lisp too, but it wasn't obvious how to retrofit Lisp to make it object oriented. CLOS for Common Lisp became somewhat popular, but its quite far from what Smalltalkers would call "object oriented", and some people feel it wasn't very Lispy either. Many Lispers like Paul Graham simply gave up on OOP. Others like me decided to code their own object-oriented dialects of Lisp. Solutions they came up with are very different. I checked the following Object-Oriented dialects of Lisp:
- RLisp - Lisp integrated with Ruby
- e7 - Lisp integrated with Python
- goo - Object-Oriented Lisp inspired by Scheme
- CLOS - Object-Oriented system built on top of Common Lisp
- I also wanted to check Coke, but it segfaulted at me, and the last thing I felt like doing was debugging broken C code.
- Define class
Pointrepresenting 2D points of vectors
- Define initializer for this class, which takes 2 arguments
yand returns a
- Define a method for stringifying
Points, like Ruby's
__str__. If possible, I wanted it to be automatically called by REPL and
(print a_point)or its equivalent.
- Define a method for adding two
Points. I wanted to call it
- Create two points, add them, and print the result.
(let Point [Class new])
(method initialize (x y)
(set! @x x)
(set! @y y))
(method to_s ()
(method + (other)
[Point new (+ [self x] [other x]) (+ [self x] [other y])]))
(let a [Point new 1 5])
(let b [Point new -2 9])
(print [a + b])
(class Point ()
(def (init self x y)
(set-self x x y y)))
(defmethod (+ (self Point) (other Point))
(+ self.x other.x)
(+ self.y other.y)))
(defmethod (print (self Point) f)
(fwrite f (format "<%s,%s>" self.x self.y)))
(def a (Point 1 5))
(def b (Point -2 9))
(print (+ a b))
(dc <point> (<any>))
(dp point-x (<point> => <num>))
(dp point-y (<point> => <num>))
(dm point-new (x|<num> y|<num>)
point-x x point-y y))
(dm point-add (p1|<point> p2|<point> => <point>)
(+ (point-x p1) (point-x p2))
(+ (point-y p1) (point-y p2))))
(dm write-point (port|<out-port> x|<point>)
(msg port "<%s,%s>" (point-x x) (point-y x)))
(dv a (point-new 1 5))
(dv b (point-new -2 9))
(write-point out (point-add a b))
(defclass point ()
((x :reader point-x :initarg :x)
(y :reader point-y :initarg :y)))
(defmethod make-point (x y)
(make-instance 'point :x x :y y))
(defmethod point-add (a b)
(+ (point-x a) (point-x b))
(+ (point-y a) (point-y b))))
(defmethod point-print ((p point))
"<~s,~s>" (point-x p) (point-y p)))
(setf a (make-point 1 5))
(setf b (make-point -2 9))
(point-print (point-add a b))
The first thing I noticed is how different these snippets look in spite of doing pretty much the same thing. The second is that RLisp and e7 are much more concise than goo and CLOS. RLisp and e7 have more syntactic extensions for OO. RLisp supports
[ ]syntax for method calls,
selffor message receiver, and
@ivarfor instance variables. e7 doesn't go that far and limits itself to
obj.attrsyntax for attribute access and method calls. In e7 like in Python all attributes are public. In RLisp like in Ruby all attributes are private, and
(attr-reader 'x)must be explicitly called.
In RLisp and e7 attributes are dynamic and aren't predeclared anywhere. In goo and CLOS list of attributes is part of class definition. goo even requires attribute types, but will happily accept
<any>. Only RLisp seemed to provide a clear way of stringifying objects.
recurring-writein goo converts standard objects to strings, but
writefor some reason ignored extensions of it. In e7 the standard way is overloading print. It seems more limited than stringification method, but it should be possible to print to string buffer instead of a real file. I have no idea what CLOS uses to stringify objects.
The most important ideological difference is treatment of method calls. RLisp makes message sends and function calls separate operations much like Smalltalk-style OOP languages. All others define methods as some kind of generic functions. This means weaker encapsulation and some deep philosophical differences. Coke also separates function calls and message sends, but like I said it was segfaulting, so I was unable to take a closer look at it.
Only in RLisp creating a class and defining stuff inside it are separate operations. Ruby is ambiguous as
class Foocan either define a new class or reopen existing class, and RLisp tried to avoid this ambiguity. Of course it's possible to do both with a simple macro. It increased verbosity of code somewhat.
In e7 class is also a constructor. In RLisp it's just an object. CLOS and goo seem to treat classes differently from other objects.
goo and CL provided only REPL environment by default, and didn't like running scripts. RLisp and e7 supported REPL and script mode without any extra hassle.
From a purely subjective point of view, I liked RLisp way most, what's not particularly surprising coming from RLisp's author ;-). Coding in e7 also felt good. On the other hand goo and especially CLOS felt rather unelegant and unpleasant.