prototypes with multiple dispatch
On New Year's Day (did I have nothing better to do?) I asked the wider
world
real life uses of non-standard method selection,
and I hinted that I had an real-life example of my own. This post does not discuss that example.
Instead, I'm going to discuss my limited understanding of another
non-standard (at least, non-CLOS-standard) method selection system,
and demonstrate what its use looks like in my current development
world. I'm talking about a prototype-based object system:
specifically, a mostly-faithful reimplementation of the Prototypes
with Multiple Dispatch
[1,2]
system found in Slate and described by
Lee Salzman and Jonathan Aldrich. I'll try to introduce the semantics
as I understand them to an audience used to class-based multiple
dispatch, but I'm left with some questions that I don't know how to
answer, not being used to programming in this way myself.
So, first, what's going on? Well, the proper answer might be to read
the linked papers, which have a relatively formal specification of the
semantics, but the high-level idea could perhaps be summarised as
finding what happens when you try to support prototype-based object
systems (where the normal way to instantiate an object is to copy
another one; where objects implement some of their functionality by
delegating to other objects; and where single-dispatch methods are
stored in the object itself) and multiple dispatch systems (where
methods do not have a privileged receiver but dispatch on all of their
arguments) simultaneously. The authors found that they could
implement some reasonable semantics, and perform dispatch reasonably
efficiently, by storing some dispatch metainformation within the
objects themselves. The example code, involving the interactions
between fish, healthy sharks and dying sharks, can be translated into
my extended-specializer CL as:
(defpvar /root/ (make-instance 'prototype-object :delegations nil))
(defpvar /animal/ (clone /root/))
(defpvar /fish/ (clone /root/))
(defpvar /shark/ (clone /root/))
(defpvar /healthy-shark/ (clone /root/))
(defpvar /dying-shark/ (clone /root/))
(add-delegation /fish/ /animal/)
(add-delegation /shark/ /animal/)
(add-delegation /shark/ /healthy-shark/)
(defgeneric encounter (x y)
(:generic-function-class prototype-generic-function))
(defmethod encounter ((x /fish/) (y /healthy-shark/))
(format t "~&~A swims away~%" x))
(defmethod encounter ((x /fish/) (y /animal/))
x)
(defgeneric fight (x y)
(:generic-function-class prototype-generic-function))
(defmethod fight ((x /healthy-shark/) (y /shark/))
(remove-delegation x)
(add-delegation x /dying-shark/)
x)
(defmethod encounter ((x /healthy-shark/) (y /fish/))
(format t "~&~A swallows ~A~%" x y))
(defmethod encounter ((x /healthy-shark/) (y /shark/))
(format t "~&~A fights ~A~%" x y)
(fight x y))
(compare figures 4 and 7 of
[1]; defpvar
is secretly just
defvar
with some extra debugging information so I don't go crazy trying to
understand what a particular #<PROTOTYPE-OBJECT ...>
actually is.)
Running some of the above code with
(encounter (clone /shark/) (clone /shark/))
prints
#<PROTOTYPE-OBJECT [/HEALTHY-SHARK/, /ANIMAL/] {10079A8713}> fights
#<PROTOTYPE-OBJECT [/HEALTHY-SHARK/, /ANIMAL/] {10079A8903}>
and returns
#<PROTOTYPE-OBJECT [/DYING-SHARK/, /ANIMAL/] {10079A8713}>
(and I'm confident that that's what is meant to happen, though I don't
really understand why in this model sharks aren't fish).
The first question I have, then, is another lazyweb question: are
there larger programs written in this style that demonstrate the
advantages of prototypes with multiple dispatch (specifically over
classes with multiple dispatch; i.e. over regular CLOS). I know of
Sheeple, another lisp
implementation of prototype dispatch, probably different in subtle or
not-so-subtle ways from this one; what I'm after, though, probably
doesn't exist: if there's no easy way of using prototype dispatch in
Lisp, it won't be used to solve problems, and some other way will be
used instead (let's call that the computer programmer's weak version
of the Sapir-Whorf hypothesis). What's the canonical example of a
problem where prototype-based object systems shine?
The second question I have is more technical, and more directly
related to the expected semantics. In particular, I don't know what
would be expected in the presence of method redefinition, or even if
method redefinition is a concept that can make sense in this world.
Consider
(defpvar /a/ (clone /root/))
(defgeneric foo (x)
(:generic-function-class prototype-generic-function))
(defmethod foo ((x /a/)) `(foo[1] ,x))
(defpvar /b/ (clone /a/))
(foo /a/) ; => (FOO[1] /A/)
(foo /b/) ; => (FOO[1] /B/)
(defmethod foo ((x /a/)) `(foo[2] ,x))
(foo /a/) ; => (FOO[2] /A/)
(foo /b/) ; => ???
What should that last form return? Arguments from the
prototype-oriented world would, I suspect, lead to the desired return
value being (FOO[1] /B/)
, as the redefinition of the method on foo
specialized to /a/
is completely irrelevant to /b/
. This is my
reading of the semantics described in
[1], for what
it's worth. Arguments from the world of generic functions and
expected behaviour would probably argue for (FOO[2] /B/)
, because
redefining a method is an action on a generic function, not an action
on a set of application objects. And the argument of my
implementation in practice (at present, subject to change) is to
signal
no-applicable-method
,
because method redefinition is the successive removal of the old
method and addition of the new one, and removal of the old method of
the generic function affects dispatch on all the objects, whereas
adding the new one affects dispatch on just /a/
.
Syndicated 2014-01-18 21:20:32 (Updated 2014-01-19 08:56:20) from notes