Sunday, August 12, 2007

Mixins

Consider typical message-passing OOP (with made up syntax):
class Rectangle {
state w, h;
method area() { ... }
method paint() { ... }
}

class Ellipse {
state major, minor;
method area() { ... }
method paint() { ... }
}

We can add new types of shapes easily, but we cannot define new operations on existing shapes without modifying existing code. In a language with algebraic data types, the situation is reversed (again, made up syntax):
data Shape = Rectangle w h | Ellipse major minor
area shape =
case shape of
Rectangle w h -> ...
Ellipse major minor -> ...

draw shape =
case shape of
Rectangle w h -> ...
Ellipse major minor -> ...

Here, we can define new operations on existing shapes but we cannot define new shapes and have the existing operations work with them.

Generic functions/words give us the best of both worlds. Here's the Factor syntax:
GENERIC: area ( shape -- )
GENERIC: draw ( shape -- )

TUPLE: rectangle w h ;
M: rectangle area ... ;
M: rectangle draw ... ;

TUPLE: ellipse major minor ;
M: ellipse area ... ;
M: ellipse draw ... ;

We can define new shapes implementing the area and draw generic words, and we can define new generic words which implement methods for rectangle and ellipse, without coupling anything together.

Now let's go back to our first message-passing OOP example, and suppose this language supports multiple inheritance of mixins. There are many definitions of what a mixin is, but for now, assume a mixin is a class with no state, just behavior.
class Shape {
abstract method triangulate();

method paint() {
... triangulate the shape with triangulate(),
then render it with a standard algorithm ...
}
}

class Rectangle < Shape {
state w, h;
method area() { ... }
method triangulate() { ... }
}

class Ellipse < Shape {
state major, minor;
method area() { ... }
method triangulate() { ... }
}

We can define new shapes which mixin the Shape mixin, but we cannot add additional mixins to the Rectangle and Ellipse data types.

In traditional Factor, the rough equivalent of the above would be to use a union class:
GENERIC: area ( shape -- )
GENERIC: triangulate ( shape -- triangle-seq )
GENERIC: draw ( shape -- )

TUPLE: rectangle w h ;
M: rectangle area ... ;
M: rectangle triangulate ... ;

TUPLE: ellipse major minor ;
M: ellipse area ... ;
M: ellipse triangulate ... ;

UNION: shape rectangle ellipse ;
M: shape draw ... triangulate, then draw the triangles ... ;

Now we can certainly define new union classes, and make either rectangles or ellipses instances of these union classes, but we cannot extend the shape union class with new shapes.

Until now, the accepted workaround ("design pattern") would be to not define a shape class at all, and instead define a draw method on the object class:
M: object draw ... triangulate, then draw the triangles ... ;

M: beizer-curve draw ... override default implementation ... ;

However, this was crufty. Defining methods on the object class is clutter. Plus, if you lose the shape union, you no longer have a way to distinguish shapes from other objects.

Enter Factor's new mixin classes feature.

Just like generic words generalize message passing OOP and case-based pattern-matching, Factor's mixin classes generalize multiple behavior inheritance and union classes.

We can define a new mixin as follows:
MIXIN: shape

We can specialize generic words on this mixin:
M: shape draw ... default implementation in terms of triangulate ...

But here's the kicker: unlike a union, where all members must be listed up-front, we can declare an existing class to be an instance of an existing mixin:
INSTANCE: rectangle shape

INSTANCE: ellipse shape

I successfully used mixins to remove some repeated code between the various implementations of sequences and associative mappings. Until now, there was no way to specialize a generic word on all sequences; now this is possible:
GENERIC: foo

M: sequence foo ... ;

A mixin is essentially a suite of methods; there is some similarity between Haskell typeclasses and Factor mixins. I'm still discovering the various applications of mixins; after Factor 0.90 is released, I intend to apply them to our I/O stream implementations and remove some boilerplate there.

So to recap, Factor's object system allows the following operations without forcing unnecessary coupling:
  • Defining new operations over existing types
  • Defining existing operations over new types
  • Importing existing mixin method suites into new types
  • Importing new method suites into existing types
  • Defining new operations in existing mixin method suites
  • Defining new mixin method suites which implement existing operations

This is a lot more general than mainstream message passing OOP and allows a wider range of problems to be expressed directly in terms of method dispatch.

1 comment:

Marcel Weiher said...

Objective-C's categories also allow you to add methods to existing classes...