(This is Part 12 of a series of posts on pLisp)
This post is almost an afterthought, since I initially thought of winding up with Part 11. But then I got to working on exceptions and an object system, and decided to write about them as well.
Exception handling turned out to be extremely trivial to implement in pLisp, on account of its support for continutations. All we need to do when we invoke a function is to capture the current continuation and create a new continuation object that incorporates the exception code and, optionally, the 'finally' code. This continuation is invoked whenever an exception occurs (through a (throw ...) special form [Update: not exactly a special form, as it's implemented in the library]). The system maintains a stack of these exception handlers, to be unwound as the exception travels up the call stack.
The only support from the core system (i.e., that requires changes to the source code of pLisp as opposed to changes to the pLisp library) is to reset the global exception handler stack after each return to the top level.
Here's the full exception system code from pLisp.lisp (in image form to avoid code wrapping and other ugly things; one of these days I should figure out how to get those pretty boxes with scroll bars in Blogger for displaying code):
"Give a man functions and procedures, and he'll never have closure.
Give a man closures, and he'll write his own object system"
(I was googling for Paul Graham's quote on closures and object oriented programming, but couldn't find it, so the above dodgy quote will have to do).
As the quote implies, a more-or-less fully functional object oriented system can be developed just with closures (and macros, of course; what will we do without them?). The meat of pLisp's object system (unfortunately named POS) is in two macros, DEFINE-CLASS and CREATE-INSTANCE, and a closure called CALL-METHOD. Though these macros look quite intimidating, they are conceptually quite simple; they create closures that store the class/instance variables and methods, and return a list of CONS pairs that map symbols to the class'/instance's methods.
Once a class or an instance has been created, we invoke methods on it by invoking CALL-METHOD.
When we define a class, we specify the following pieces of information:
- The name of the class
- The name of its superclass
- List of instance variables with their initial values and getter/setter names
- Ditto for class variables
- Closures that perform initialization for the class and its instances
- List of class methods
- List of instance methods
;name of the class
;name of its superclass
;list of class variables
;list of instance variables
((A NIL GET-A SET-A))
NIL ;class initialization code
(LAMBDA (VAR) (SET A VAR))
((AREA () (* A A))))
;MAKE-INSTANCE is a convenience macro
(DEFINE A-SQUARE (MAKE-INSTANCE SQUARE))
;sets instance variable A to 10
(CALL-METHOD A-SQUARE 'SET-A 10)
;invokes the function AREA that returns (* A A), i.e. 100
(CALL-METHOD A-SQUARE 'AREA)
Inheritance, data encapsulation, information hiding -- all there in about 100 lines of pure library code (polymorphism via the dynamic typing already in place).
One current deficiency is the inability to refer directly to class variables from instance methods; the workaround is to invoke CALL-METHOD on the class with the relevant symbol/message.