Inheritance is done
Thursday, April 3, 2008
I’ve implemented the last major missing piece of functionality in the new inheritance system, and that is method dispatch which is compatible with inheritance.
To make inheritance as useful as delegation, there needs to be an easy way to call the next applicable method. With delegation, this was done entirely in the language itself:
M: my-gadget graft*
dup delegate graft* ... do more stuff ... ;
However the delegate method would not see the current object on the stack; hence the “slicing problem”. With inheritance, there is a special word:
M: my-gadget graft*
dup call-next-method ... do more stuff ... ;
Now call-next-method
is a parsing word. It expands to this:
M: my-gadget graft*
dup \ my-gadget \ graft* (call-next-method) ... do more stuff ... ;
At compile time, (call-next-method)
expands into code which checks if
the top of the stack is of the correct type for the current method; if
not, it throws an error. If the check succeeds, it calls the next
applicable method.
While I added this feature primarily for use with tuple inheritance, it also works in two other instances:
- Predicate class inheritance
- Union classes
The first case was a real pain to deal with before today, because there there was no way to call the next method on a parent predicate class. Suppose you had two predicate classes:
PREDICATE: foo < word ... ;
PREDICATE: bar < foo ... ;
And a generic word:
GENERIC: blah
M: foo blah ... ;
M: bar blah ... ;
If bar
wanted to do something and then call the foo
method, the only
way was with a design pattern:
: foo-blah ... ;
GENERIC: blah
M: foo blah foo-blah ;
M: bar blah ... foo-blah ;
This is smelly. Now you can just do:
GENERIC: blah
M: foo blah ... ;
M: bar blah ... call-next-method ;
The situation with unions is similar. However, because unions give you what amounts to multiple inheritance, there is a complication.
Since next method is computed statically, and this gives different semantics than CLOS. Suppose we have the following code:
TUPLE: a ;
TUPLE: b ;
TUPLE: c ;
UNION: x a b ;
UNION: y a c ;
UNION: z x y ;
We can represent the subtype relationships here as follows:
Z
/ \
X Y
/\ /\
B A C
Suppose we have a generic word:
GENERIC: funky* ( obj -- )
M: z funky* "z" , drop ;
M: x funky* "x" , call-next-method ;
M: y funky* "y" , call-next-method ;
M: a funky* "a" , call-next-method ;
M: b funky* "b" , call-next-method ;
M: c funky* "c" , call-next-method ;
: funky [ funky* ] { } make ;
Now the following two results are straightforward:
( scratchpad ) T{ b } funky .
{ "b" "x" "z" }
( scratchpad ) T{ c } funky .
{ "c" "y" "z" }
However, suppose you have an instance of a
on the stack. The next
method after a
is either x
or y
, and this depends on the phase of
the moon; the two classes overlap but neither is a subset of the other,
so we have some ambiguity here. However, for the sake of argument,
suppose the next method is x
. Now, statically, the next method after
x
is z
. However, given that we have an a
on the stack, the method
on y
is also applicable, so if we computed it dynamically, the next
method would be y
and then z
. So to summarize, right now, we get
one of the following two results:
( scratchpad ) T{ a } funky .
{ "a" "x" "z" }
( scratchpad ) T{ a } funky .
{ "a" "y" "z" }
However, it would be more desirable to get one of the following:
( scratchpad ) T{ a } funky .
{ "a" "x" "y" "z" }
( scratchpad ) T{ a } funky .
{ "a" "y" "x" "z" }
However, doing so would require expanding (call-next-method)
into a
type dispatch, not a direct call. Implementing this efficiently and
updating all occurrences of (call-next-method)
as classes are
redefined is still an open problem, and one that I will tackle later.
The current static behavior will suffice for now.
Now that the code is done, I still need to write documentation and
update the core to actually use inheritance in place of delegation.
After that, I will return to the web framework and let things settle
down a bit. Once I’m happy with the stability of both the core and the
state of the web framework, I will tackle multi-methods; what this
really means is efficient implementation of multi-methods, given that
an inefficient implementation already exists in extra/
. This presents
its own set of unique challenges but I look forward to solving that
problem.
Once Factor has multi-methods, our object system will be comparable in power to CLOS, however I believe it will offer genuine improvements and innovations in several respects. Furthermore, the implementation will be a lot simpler than PCL.