Paul Khuong mostly on Lisp

rss feed

Thu, 15 Dec 2011

 

Initialising structure objects modularly

I use defstruct a lot, even when execution speed or space usage isn’t an issue: they’re better suited to static analysis than standard-objects and that makes code easier to reason about, both for me and SBCL. In particular, I try to exploit read-only and typed slots as much as possible.

Structures also allow single-inheritance – and Christophe has a branch to allow multiple subclassing – but don’t come with a default protocol like CLOS’s make-instance/initialize-instance. Instead, we can only provide default value forms for each slot (we can also define custom constructors, but they don’t carry over to subtypes).

What should we do when we want to allow inheritance but need complex initialisation which would usually be hidden in a hand-written constructor function?

I’ll use the following as a completely artificial example. The key parts are that I have typed and read-only slots, that the initialisation values depend on arguments, and that we also have some post-processing that needs a reference to the newly-constructed structure (finalization is a classic).

(defstruct (foo 
            (:constructor %make-foo)) 
  (x (error "Missing arg") :type cons 
                           :read-only t) 
  (y (error "Missing arg") :read-only t)) 
 
(defun make-foo (x y) 
  ;; non-trivial initial values 
  (multiple-value-bind (x y) (%frobnicate x y) 
    (let ((foo (%make-foo :x x :y y))) 
      ;; post-processing 
      (%quuxify foo) 
      foo)))

The hand-written constructor is a good, well-known, way to hide the complexity, as long as we don’t want to allow derived structure types. But what if we do want inheritance?

One way to work around the issue is to instead have an additional slot for arbitrary extension data. I’m not a fan.

Another way is to move the complexity from make-foo into initialize-foo, which mutates an already-allocated instance of foo (or a subtype). I’m even less satisfied by this approach than by the previous one. It means that I lose read-only slots, and, when I don’t have sane default values, typed slots as well. I also have to track whether or not each object is fully initialised, adding yet more state to take into account.

For now, I’ve settled on an approach that parameterises allocation. Instead of calling %make-foo directly, an allocation function is received as an argument. The hand-written constructor becomes:

(defun make-foo (x y 
                 &optional (allocator ’%make-foo) 
                 &rest     arguments) 
  ;; the &rest list avoids having to build (a chain of) 
  ;; closures in the common case 
  (multiple-value-bind (x y) (%frobnicate x y) 
    ;; allocation is parameterised 
    (let ((foo (apply allocator :x x :y y arguments))) 
      (%quuxify foo) 
      foo)))

This way, I can define a subtype and still easily initialize it:

(defstruct (bar 
            (:constructor %make-bar) 
            (:include foo)) 
  z) 
 
(defun make-bar (x y z) 
  (make-foo x y ’%make-bar :z z))

The pattern is nicely applied recursively as well:

(defun make-bar (x y z 
                 &optional (allocator ’%make-bar) 
                 &rest     arguments) 
  (apply ’make-foo x y allocator :z z arguments))

Consing-averse people will note that the &rest arguments are only used for apply. SBCL (and many other implementations, most likely) handles this case specially and doesn’t even allocate a list: the arguments are used directly on the call stack.

I’m sure others have encountered this issue. What other solutions are there? How can the pattern of parameterising allocation be improved or generalised?

posted at: 18:29 | /Lisp | permalink

Made with PyBlosxom Contact me by email: pvk@pvk.ca.