Getting Started with EK9 · Chapter 18
Traits and Composition
Abstract Types, Subtypes and Polymorphism gave one way for many types to share a contract: an abstract class with subtypes. This chapter gives EK9's preferred way, the trait, and the style of building it encourages: composition rather than deep inheritance.
A trait is a named set of methods — with or without bodies — and no state of its own, which any class can take on. The name comes from research on traits as composable units of behaviour: reusable behaviour, with no fields, that can be mixed into classes and combined without the complications of inheriting from several parents.1
Aside — why not "interface" or "protocol"? Other languages use those words for similar ideas. An interface, in its original Java sense, lists method signatures and nothing else; an EK9 trait can also carry method bodies, so the word would undersell it. Protocol, the Swift word, already means network protocols in computing, and EK9 has networking types, so the word would collide.
Behaviour that cuts across a hierarchy
Traits are easiest to see where ordinary class inheritance struggles. Think about animals. A sensible
class hierarchy might put Bird and Reptile under a common Animal. Now ask: which animals can
swim? A penguin can, and so can a crocodile — but a penguin is a bird and a crocodile is a reptile,
so "can swim" does not belong on any one branch. Swimming, flying and walking are behaviours that
cut across the hierarchy, and a single line of inheritance cannot express them.
Traits can. Here each behaviour is a trait, and each animal takes the ones it has:
#!ek9 defines module book.animals defines trait Named name() as pure abstract <- rtn as String? Swimmer with trait of Named swim() as pure <- rtn as String: "paddles through the water" Flyer with trait of Named fly() as pure <- rtn as String: "takes to the air" Walker with trait of Named walk() as pure <- rtn as String: "walks on land" defines class Animal as abstract name() as pure abstract <- rtn as String? category() as pure abstract <- rtn as String? default operator ? Bird extends Animal as abstract override category() as pure <- rtn as String: "bird" default operator ? Reptile extends Animal as abstract override category() as pure <- rtn as String: "reptile" default operator ? Penguin extends Bird with trait of Swimmer, Walker override name() as pure <- rtn as String: "penguin" override swim() as pure <- rtn as String: "flies through the water on stubby wings" default operator ? Duck extends Bird with trait of Swimmer, Flyer, Walker override name() as pure <- rtn as String: "duck" default operator ? Crocodile extends Reptile with trait of Swimmer, Walker override name() as pure <- rtn as String: "crocodile" override swim() as pure <- rtn as String: "glides silently through the water" default operator ? defines program Show() stdout <- Stdout() penguin <- Penguin() penguinIs <- `The ${penguin.name()} (a ${penguin.category()})` stdout.println(`${penguinIs} ${penguin.swim()} and ${penguin.walk()}.`) duck <- Duck() duckIs <- `The ${duck.name()} (a ${duck.category()})` stdout.println(`${duckIs} ${duck.swim()}, ${duck.fly()} and ${duck.walk()}.`) swimmers <- List() of Swimmer swimmers += Penguin() swimmers += Crocodile() for s in swimmers stdout.println(`the ${s.name()} ${s.swim()}`)
The penguin (a bird) flies through the water on stubby wings and walks on land.
The duck (a bird) paddles through the water, takes to the air and walks on land.
the penguin flies through the water on stubby wings
the crocodile glides silently through the water
Several ideas are at work here; take them one at a time.
A trait carries behaviour, but no state. swim, fly and walk each have a real method body.
What a trait does not have is fields.
A trait's method is a default, not a fixture. In the output, the duck "paddles through the water" —
the swim the Swimmer trait supplies — but the penguin "flies through the water on stubby wings" and
the crocodile "glides silently". Each of those is the class overriding the trait's method with its
own version, as a subtype overrode an abstract method in the previous chapter. So a trait provides a
shared default, and any class that has the trait can keep it or replace it. Because it is a real
override, it works with polymorphism: when the loop over swimmers calls swim(), each animal
answers in its own way.
Note. A trait method does not have to have a body. Leave the body off and the method is abstract, as in an abstract class, and every class that takes the trait must supply its own version or it will not compile (
E07130, the same error a class gets for an unimplemented abstract method). That is how a trait says "you must provide this; I have no sensible default".swimhas an obvious default, but aname()or aprice()might not. You can mix the two — a method with a body, written in terms of an abstract one — so the trait supplies the shape of some behaviour and each class fills in the one piece only it knows.
The class hierarchy is separate and ordinary. Animal is abstract; Bird and Reptile extend
it (and are still abstract, because neither says what its name is); each concrete animal extends one
of those. A class may extend only one parent class.
The join is the important part. A concrete animal extends its one parent and takes any number
of traits with with trait of. Penguin extends Bird with trait of Swimmer, Walker is a bird that can
swim and walk; Duck adds Flyer; Crocodile is a reptile that swims and walks. Behaviours are
mixed in, in whatever combination fits, regardless of where the animal sits in the class tree. That
is what single inheritance cannot do.
You can group things by what they do. swimmers is a List of Swimmer, and it holds a Penguin
and a Crocodile — a bird and a reptile — because what they share is not a class but a behaviour.
A trait can build on another trait. The loop prints ${s.name()} as well as ${s.swim()}, yet
s is only known to be a Swimmer. Swimmer with trait of Named says every Swimmer is also
Named, so any concrete swimmer must supply name(), and a List of Swimmer gives you both. Named
is the small shared contract that Swimmer, Flyer and Walker all build on.
List each trait once. Penguin gets Named through Swimmer, so it does not list Named itself;
writing with trait of Swimmer, Walker, Named is an error (E02090: "'Named' is already a trait of
'Swimmer'; list it once").
In the previous chapter, polymorphism ran along a single line: a Circle could stand in for a Shape
because it was a kind of shape. Traits add other lines. A Penguin can be used as an Animal (its
place in the class tree), and as a Swimmer, and as a Walker — three independent contracts
it satisfies at once. Traits extend polymorphism beyond the one hierarchy a class belongs to: an
object can be used through every contract it fulfils.
The reverse is checked too. A Penguin never took Flyer, so penguin.fly() does not compile. The
behaviours a type has are checked, not assumed: a Penguin can be used as a Swimmer, never as a
Flyer.
Composition without the boilerplate
Traits also support the reuse style EK9 prefers. Suppose you want to wrap something that has a trait
and change one part of its behaviour, leaving the rest as it was. That is the Decorator pattern,
one of the classic designs catalogued by the "Gang of Four": you place an object inside another that
offers the same methods, so the wrapper can be used anywhere the original could, adding to or
changing what passes through. Because the wrapper is the same kind of thing, decorators can be stacked,
each layer adding a little.2 Here a View has a title and a body, and Framed is
another View that puts a border around the body of the view it wraps:
Framed with trait of View by inner inner as View? default private Framed() as pure Framed() as pure -> viewToWrap as View require viewToWrap? inner :=? viewToWrap override body() as pure <- rtn as String: `*** ${inner.body()} ***` default operator ?
by inner is the important part. It says "I have the trait View, and its methods are delegated
to my inner field." So Framed gets title — and every other View method — forwarded
automatically to whatever inner holds. It writes only the method it wants to change, body. Given
Framed(Home()), title() returns Home (from the wrapped view) and body() returns
*** Welcome ***.
This is composition: building a thing out of another thing it holds, rather than out of a parent
it inherits from. It is EK9's preferred way to reuse behaviour. The advice to prefer composition over
inheritance is long established; the obstacle has been the typing — to wrap an object you normally
write a forwarding method for every one of its operations, and add another each time the wrapped type
grows. by inner writes them for you.3 inner is held by reference, not copied, which
is right here: a wrapper is meant to refer to one particular object and speak for it.
When two traits collide
A type can hold several traits, so two unrelated traits might each declare a method with the same name. Which version should win? This name clash is what makes inheriting from several parents risky, and EK9 refuses to guess: the combining type must resolve it itself. (Inheriting one and the same method through two routes — where both traits got it from a shared trait — is not a clash: there is only one version, so nothing to choose.)
Widget with trait of Priced, Monitored override lowCost() as pure <- rtn as Boolean: Priced.lowCost()
Both Priced and Monitored declare lowCost, so Widget must write its own lowCost and say what
it means. It can use one trait's version by name — Priced.lowCost(), or Monitored.lowCost() —
combine both, as in Priced.lowCost() and Monitored.lowCost(), or write a new implementation of its
own. What it may not do is leave the method out and let EK9 pick. Leave it out and the program does
not compile (traitClash.ek9, in the error-examples folder):
EK9 : Error : E06150: 'Widget' on line 10 position 4: method 'public Boolean <- lowCost()' on 'Monitored' and 'Priced' : conflicting methods to be resolved
See: ek9 -h E06150
So a clash is never a surprise while the program runs, and never settled by an arbitrary winner: it is a compile error that names the clash for you to settle. There is more to combining traits well — Scala, which brought traits to a wide audience, has a detailed scheme for stacking them in order — but that is a topic for the second volume. The rule here: EK9 never chooses for you.
Sealing a family
Sometimes the set of types that implement a trait is meant to be closed: exactly these, no more. A
trait says so with allow only:
Payment allow only Card, Cash amount() as pure <- rtn as Integer?
Only Card and Cash may be Payments. A third type that tries — here a Cheque
(notAllowed.ek9, in the error-examples folder) — is rejected at compile time:
EK9 : Error : E05240: 'Cheque' on line 17 position 4: 'Cheque' is not in 'Payment' allow only list: type is not permitted to extend/implement this sealed type
See: ek9 -h E05240
That is useful when code elsewhere depends on handling every kind: a closed set the compiler can hold you to, rather than an open one that might grow without your knowing.
Try it yourself
- Add a
Bat extends Mammal with trait of Flyer, Walker(you will need aMammalclass too). It flies and walks like a bird, though it is a mammal: the behaviour does not depend on the branch. - Call
penguin.fly()and read the error. - Build a
List of Flyer, add aDuckand something new, and confirm aPenguinis not allowed in it.
Recap
A trait is a stateless unit of behaviour that a class takes with with trait of. It can carry
method bodies, which a class may keep or override, or abstract methods every class must supply; and a
class can take many traits, though it extends only one parent class. Traits capture behaviour that
cuts across a class hierarchy — Swimmer shared by a bird and a reptile — so you can group and use
things by what they do, not only by what they are. Each trait is listed once (E02090). Traits
are also the basis of EK9's preferred style, composition: by delegates a trait's methods to a
field, so you can wrap and reuse without writing forwarding methods. When traits clash, EK9 makes you
resolve it (E06150); when a family should be closed, allow only seals it (E05240). Next,
Components: the larger building blocks a program is assembled from.
-
The term and the mechanism come from Nathanael Schärli, Stéphane Ducasse, Oscar Nierstrasz and Andrew P. Black, "Traits: Composable Units of Behaviour," ECOOP 2003, where a trait is a group of methods with no state, a building block for classes that composes cleanly and resolves conflicts explicitly. The ideas were first implemented in the Squeak dialect of Smalltalk, and were brought to a wide audience by Scala, whose standard text (Martin Odersky, Lex Spoon and Bill Venners, Programming in Scala) devotes a chapter to exactly the two uses we see here: mixing behaviour into classes, and widening a "thin" interface to a "rich" one by building default methods on abstract ones. ↩
-
The Decorator is one of the twenty-three patterns catalogued in Gamma, Helm, Johnson & Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (1994). Its intent, in their words, is to "attach additional responsibilities to an object dynamically" by wrapping it in another object of the same interface — a flexible alternative to subclassing for extending behaviour. EK9's
bydelegation is what makes it cheap: the forwarding methods a decorator would otherwise need are written for you. ↩ -
"Favour object composition over class inheritance" is one of the two founding principles of the Gang of Four's Design Patterns (Gamma, Helm, Johnson & Vlissides, 1994). The case against leaning on inheritance is the fragile base class problem — a change to a base class can break its subclasses, and you cannot tell it is safe by reading the base alone — and the observation, shown by William Cook, Walter Hill and Peter Canning in "Inheritance Is Not Subtyping" (POPL 1990), that the useful part (substitutability) is separable from the risky part (implementation inheritance). EK9 keeps the first through traits and steers reuse toward composition, which
bymakes cheap. ↩
In the EK9 reference: Composition in EK9, Design note: why EK9 favours composition over inheritance, Traits in EK9