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:
Objective-C's categories also allow you to add methods to existing classes...
Post a Comment