One aspect of the type system that’s always left me unsatisfied is its asymmetry against traditional object-oriented languages. Most OO languages formally recognize inheritance within the type system, but not composition. Given that Effes formally recognizes composition, shouldn’t it not recognize inheritance?
This is important to me for more than just aesthetic reasons. Recognizing both patterns makes for a more complicated type system, but worse, it gives the programmer a too-easy crutch. One of the reasons I turned to Haskell when I was interested in learning about functional programming was that I wanted to force myself to really start thinking in FP terms. If I were learning on a language like Scala, which combines OO and FP patterns, it’d be too easy to fall back on familiar ways of looking at a problem.
In the same way, I want Effes to force me into thinking with a composition-based perspective, rather than letting me have another inheritance-based language with a shot of composition flavoring.
The hurdle, though, has been polymorphism. It’s useful to have a method that takes
Sizeable objects, whether they’re
Map or anything else that’s
Sizeable. It’s also nice to have that
size method on both
My solution is to replace “
Sizeable” with “
type List[A]: has Size add A -> List[A] -- etc...
For a user of
List to get to the
size method, they’ll need to access its
Size component, which can be done explicitly with
(list @ Size) size. But, if the
Size component doesn’t conflict with any other of
List’s components, you can implicitly access it:
list size. And similarly, if a method takes a
Size argument, you can explicitly give it the list’s
Size component by calling
theMethod (list @ Size), but you can also just call
theMethod list, and the compiler will figure out that you want to pass it the
A nice side benefit of all this is that it provides a nicer answer to the question of conflicting components, which I addressed in earlier posts. Rather than handling conflicts at composition time by knocking out some components, I’ll allow the conflict there, and force the user into stating which component they want, when there’s a conflict. So for instance, if
Set both have an
add method, you can’t write
listAndSet add foo. You have to explicitly call out the component you want:
(listAndSet @ List[Foo]) add foo.
There are two syntax details I have yet to work out with this all-composition scheme.
The first involves cleaning up the code when a type has only one component:
List[A], for instance. Everything is fine from a useage perspective, but it’s a bit awkward to write out:
type ConsList[A]: has List[A]: -- all of the ConsList code goes here
So, I’m thinking of allowing a special “is-a” statement for this situation, which just lets you inline the second line in the above:
type ConsList[A] is List[A]: -- all of the ConsList code goes here
The second is in cleaning up implementations of nested types. Remember how
List had a
Size component above? Does that mean we have to implement it as:
type ConsList[A] is List[A]: add elem: ... Size: size: ...
or can we just write:
type ConsList[A] is List[A]: add elem: ... size: ...
My inclination here is to mirror the call site rules: you can inline the method definitions for a given component if that component doesn’t conflict with any other components. That keeps things simple, consistent and clean.