nótaí de devos

Trait Composition (in scala)

Traits are powerful magic when combined with self type annotations and abstract type members. But is this malignant or benevolent magic? I have some tips for dealing with, or avoiding, out-of-control trait compositions.

In the beginning Odersky's idea was that traits should work in the small and in the large. That is: as bases of ordinary objects and as modules making up a system.

Subsequent experience showed that you can get into a confusing mess with traits as modules. Well, I know I have.

There have been many explanations of the cake pattern over the years, which attempt to keep you organised. But I have learned then forgotten about the layers and the slices. It is all too much boiler plate.

Here is my take on trait composition.

Mating Rituals

Trait composition is a way of mating declarations with definitions. The alternative is explicit or implicit parameter passing.

Declaration

Definition

Trait Composition

abstract value member

concrete value member

abstract type member

concrete type member

Parameter Passing

formal value

actual value

formal type

actual type

Which of these fundamental mechanisms pleases you depends on the situation but is also a matter of taste.

Anatomy of a Simple Composition

Consider this API which has declarations for createNew, Robot and battle:

Declarations are mated to definitions and a user of the API is mated with a particular implementation. I think this is easy enough.

Abstract Types

Note the abstract type member, Robot in the API trait. Trait composition and abstract type members go hand in hand.

At note 1 you can see that the type of Robot is left open except that it must support RobotOps. In java, by comparison, that cannot be expressed.

At note 2 battle is called with a parameter of this abstract type.

But at note 3 the same type is concrete and the parameter of battle is known have a strength member. A typical java implementation would involve a down cast from an interface to a concrete class at this point.

Module Hell

Confusion arises when there are more modules and more complicated interdependencies. It is difficult to pin down the cause of module hell but you know it when you experience it.

Some symptoms I have noticed:

The distinctions between API, user and implementation roles seem to blur.

You repeatedly list the same parents for many traits.

You have long lists of parent traits and you forget which are really needed and what each provides.

Name clashes among members of the trait composition become more frequent.

The Kitchen Sink

Here is a fairly brutal way to break out of module hell:

Identify the pluggable modules of the composition. Each has one API trait and (potentially) several alternative implementation traits.

Identify the fixed modules of the composition. Each has a trait defining both an API and its implementation. (It may depend on other traits so is not self contained.)

Define a kitchen sink trait that extends all API traits, pluggable and fixed, but not the implementation traits.

The definition of Assembly, Main and Test remain the same but the module can be pulled out and reused.

Hesitant Recommendations

I hesitate to give firm advice in this area.

But if you are in module hell you might consider applying the kitchen sink approach and then improve the precision of the self types where appropriate.

This might be a good way to start a modular design too. Initially everything depends on the kitchen sink which makes it easy to move things around during early development. The self types can be progressively narrowed as the module boundaries firm up.