Scheme object systems: POS

I'm no OOP fan (much less a fan of single-dispatch OOP), but sometimes I miss the implicit lexical scope that single-dispatch provides for methods. Take something as simple as

class Rect {
  int top, left, bottom, right;
  int area () const {
    return (top - bottom) * (right - left);
  }

Most Scheme object systems (see for example the Chicken OOP section) turn the area() method body into something tedious along the lines of


(* (- (slot-ref self 'top) (slot-ref self 'bottom))
   (- (slot-ref self 'right) (slot-ref self 'left)))

or, with objects implemented as closures,

(* (- (self 'top) (self 'bottom) ...) ...)

Until recently, I thought that short of codewalker-based macros, nothing could restore the terseness of single-dispatch methods.

Well, I've discovered Blake McBride's POS (portable object system). With POS, you can write

(define-class Rect (ivars top left bottom right)
  (imeths
    (set-top! (self val) (set! top val)) ...
    (get-area (self) (* (- top bottom) (- right left)))))

POS is a set of pure R5RS macros, and correctly interacts with other syntax-rules macros (e.g. macros can appear within method bodies). The trick is not only to represent the objects as closures, but to also expand method bodies inside the closure:

;; not the actual POS expansion -- just an illustration
(define (make-rect)
  (let ((top #f) (left #f) (bottom #f) (right #f))
    (define self
      (lambda (meth-name . args)
        (case meth-name
          ((set-top!)
           (apply (lambda (self val) (set! top val))
                  (cons self args))) ...
          ((get-area)
           (apply (lambda (self)
                    (* (- top bottom) (- right left))
                  (cons self args)))))))

    self))

This way, methods can access instance variables as simple literals. Each object is a dispatch function that closes over those variables.

POS is very useful, and I plan to add default getters and setters, as well as a way to convert between the closure representation and a-lists. This should help with persistence, among other things.

POS has a couple of extra features (inheritance, access to the parent object, class methods) but really is a light-weight system. The major downside is that methods (and instance variables) can no longer be added dynamically, since it's impossible to inject code (or data) into a closure.

Update: see the comments for yet another way of simulating an implicit "this" argument.

Tags: 

Comments

Thanks for the comments, Graham and Pascal. I conjecture that with-slots and with-accessors are implemented using the technique described by Graham. I've implemented a similar with-* macro in Scheme a while ago:

(define-syntax with-hash-table
  (syntax-rules ()
    ((_ ht (var ...) body ...)
     (let ((var (hash-table-ref ht 'var)) ...)  ;; let == lambda
       (define result (begin body ...))
       (hash-table-set! ht 'var var) ...  ;; final step: update ht
       result))))

One caveat: the original method works fine for implementing accessors. However, we need an extra step to implement modifiers, since instance variables are only copied (passed by value) to lambda arguments. In this final step (see code above), the lambda arguments are copied back to the instance variables. But, of course, this final step can interact badly with any call/cc and dynamic-wind calls in the method body.

It's not impossible to inject code into a closure; you can pass in lambdas which can be stored, for example, in a class-wide a-list; this would be searched after all static methods have been exhausted. The trick is that the lambdas must take an argument list matching all the variables that are bound within the object's closure, e.g.

(add-method-to Rect get-perimeter
 (lambda (top left bottom right)
   (* 2 (+ (- right left) (- bottom top)))))

Then have the Rect object "call" the lambda with the appropriate arguments if this method is selected. (This does imply that the list of attributes (bound variables) is fixed at class-definition time.)

Perhaps you could define some syntax when Rect is defined, e.g. "add-method-to-Rect", which would expand to an expression like the one above, but would be simpler to write:

(add-method-to-Rect get-perimeter  ; where add-method-to-Rect is syntax...
  (* 2 (+ (- right left) (- bottom top))))

CLOS (for Common Lisp) has two forms with-slots and with-accessors that give you a similar expressiveness to a certain degree. With those forms, you can write this:

(defmethod area ((rect rect))
  (with-slots (top bottom right left) rect
     (* (- top bottom) (- right left))))

It's a bit tedious to have to reintroduce the variable names, but for longer methods this doesn't matter that much. On the plus side, you can introduce variable names for slots from several objects within the same method.

See http://www.lispworks.com/documentation/HyperSpec/Body/m_w_slts.htm and http://www.lispworks.com/documentation/HyperSpec/Body/m_w_acce.htm for more examples.

With the introduction of identifier macros in R6RS, it should be a piece of cake to introduce something similar for any object system in Scheme.

Add new comment

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <pre> <code> <kbd> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.