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". swim has an obvious default, but a name() or a price() 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

  1. Add a Bat extends Mammal with trait of Flyer, Walker (you will need a Mammal class too). It flies and walks like a bird, though it is a mammal: the behaviour does not depend on the branch.
  2. Call penguin.fly() and read the error.
  3. Build a List of Flyer, add a Duck and something new, and confirm a Penguin is 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.

  1. 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. ↩

  2. 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 by delegation is what makes it cheap: the forwarding methods a decorator would otherwise need are written for you. ↩

  3. "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 by makes cheap. ↩