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 List
, Map
or anything else that’s Sizeable
. It’s also nice to have that size
method on both List
and Map
.
My solution is to replace “List
is-a Sizeable
” with “List
has-a Size
component:”
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 Size
component.
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 List
and 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: ConsList[A]
“implements” 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.