Thursday, April 03, 2008

Inheritance is done

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:

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 ... ;


M: foo blah foo-blah ;

M: bar blah ... foo-blah ;

This is smelly. Now you can just do:

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:
/ \
/\ /\

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: