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.
No comments:
Post a Comment