skip to content

Traits & Mixins

Composing behavior into classes without inheritance: mixins linearized into the hierarchy, traits flattened with explicit conflict resolution. A standard probe when designs outgrow single inheritance.

on this pageshow

questions

2

Reusable bundles of methods come in two flavours: ones copied into the composing class so that a name clash is an error you must resolve, and ones inserted into the lookup chain so that composition order decides the winner. Contrast the two models and name languages of each kind.

level: middleimportance: should knowfreq 40%

answer

  1. flatten = order-irrelevant, clash is an error
  2. chain = position matters, forwarding possible
  3. exclusion + aliasing are the flattening operators
  4. Ruby include behind, prepend in front
  5. Rust: collision only at the call site

basics

~20 s

Flattening (Pharo traits, PHP traits, Rust traits): methods are copied in, composition order is irrelevant, and a clash is reported and resolved by exclusion or aliasing. Chain insertion (Ruby modules, Scala traits): the unit becomes a link in the lookup chain, so order decides and forwarding continues along it.

solid answer

~50 s

- **Pharo/Squeak traits** - the original model - define composition as *flattening*: the composed class behaves exactly as if the methods had been typed into it, and the composition operators are exclusion and aliasing. Order-independence is the defining property. - **PHP traits** flatten the same way. Two used traits defining `say()` is a fatal error unless resolved with `insteadof` and `as`; the class's own method beats the trait, and the trait beats the inherited parent. - **Rust traits** flatten and never collide at the definition; ambiguity appears only at a call site and is fixed with `<T as Trait>::m(x)`. - **Ruby modules** are mixins, not traits: `include` inserts the module into the ancestor chain behind the class, `prepend` in front of it, later inclusions win, and `super` walks that chain. - **Scala traits** linearize right-to-left, and `super` in a trait targets the next trait, which is what enables stackable wrapping. Flattening buys order-independence; chaining buys "wrap whatever came before".

code

scala · 8 lines
scala
trait Base  { def say: String = "base" }
trait Loud  extends Base { override def say = "LOUD:"  + super.say }
trait Quiet extends Base { override def say = "quiet:" + super.say }

class A extends Loud with Quiet   // say == "quiet:LOUD:base"
class B extends Quiet with Loud   // say == "LOUD:quiet:base"
// note: two UNRELATED concrete says (no common Base, no override)
// would instead be a compile error about conflicting members

go deeper

for a junior

Know that some systems copy methods in and report clashes, while others insert the unit into the lookup chain so order decides, and name one language of each.

for a middle

State the defining property of each model and give the resolution operators - exclusion/aliasing versus insertion position and forwarding - with a named language for each.

for a senior

Discuss local reasoning versus decoration power, note Scala's hybrid rule that only overriding members are ordered silently, and describe how each model fails when two libraries collide.

for a principal

Argue which model you would put in a platform where units are written by independent teams, weighing loud-conflict cost against the ability to compose cross-cutting behaviour, and mention Rust's coherence rules as a structural alternative.

## Two families that look alike from the outside Both models let you write a bundle of methods once and attach it to unrelated classes. The difference is what the composition *means*. **Flattening.** The composed class is defined to be equivalent to a class in which all the borrowed methods were written directly. Nothing is inserted into the inheritance chain, so there is no notion of the unit's "position", and the same set of units composed in any order gives the same class. Because there is no order, a name appearing in two units cannot be resolved by precedence - the language must report it and offer explicit operators to resolve it. **Chain insertion (mixin).** The unit is spliced into the lookup chain as a pseudo-ancestor. Method lookup then works exactly as it always did, walking the chain until it finds a match, so the winner is whichever unit was inserted closest. Order is now semantically significant, and because the unit has a position, a unit can forward to whatever sits after it. ## Concrete languages **Pharo/Squeak (Scharli, Ducasse et al.)** invented the trait in the flattening sense. A trait provides methods, requires others, and carries no state. Composition uses two operators: **exclusion** (`T - {#m}`), which removes a method so the other provider wins, and **aliasing** (`T @ {#newName -> #m}`), which keeps both under different names. The flattening property is stated as a design guarantee: a method's meaning does not depend on which traits it was composed with. **PHP** adopted the same shape. `use A, B;` where both define `say()` is a fatal compile-time error; you write `use A, B { A::say insteadof B; B::say as loudSay; }`. Precedence is fixed and small: the class's own definition wins over any trait, a trait wins over an inherited parent method. Note that this is precedence between *layers*, not between traits - between two traits there is no ordering rule at all, which is the flattening property showing through. **Rust** is flattening with the conflict pushed even later. Implementing two traits that both define `m` for one type is entirely legal. Only a call `x.m()` with both traits in scope is ambiguous, reported as multiple applicable items, and disambiguated with `Trait::m(&x)` or `<T as Trait>::m(&x)`. Rust also has coherence rules, so two independent crates cannot both provide the same trait implementation for the same type - the "two libraries collided" scenario that PHP and Ruby must handle by hand is structurally impossible. **Ruby** is the canonical mixin. `include M` inserts `M` into the ancestor chain *after* the class, so the class's own methods win; `prepend M` inserts it *before* the class, so the module can wrap the class's own method and call `super` to reach it. Multiple `include`s stack, with the most recently included nearest. `Module#ancestors` prints the resulting chain, and the whole design is order-sensitive by construction. **Scala** calls its unit a trait but composes it by linearization, which is chain insertion: `class C extends A with B` places `B` ahead of `A`, and `super` inside a trait means the next trait in the concrete class's linearization. That is what makes the stackable-modification idiom possible - each trait decorates the one behind it. Scala adds a guard the other chaining languages lack: it only lets linearization pick a winner when the members are related by `override`. Two unrelated concrete members with the same signature is a compile error demanding an explicit override, so accidental collisions are loud while intentional stacking is silent. ## Trade-offs Flattening gives **local reasoning**: read the class, read each unit, and you know the result; reordering the composition clause cannot change behaviour; every collision is surfaced at compile time. What it cannot express is "wrap the implementation that would otherwise have been used", because there is no ordering and therefore no previous implementation to name. A logging or memoizing decorator has to be written as an explicit wrapper object. Chain insertion gives exactly that missing power - each unit can intercept and forward, so cross-cutting concerns compose into a pipeline - and it makes accidental collisions cheap to *ignore*, which is also its danger: nothing tells you that a module you included silently displaced a method from another. It also makes behaviour non-local, since the effective chain is a property of the concrete class, decided far from either unit. ## Answering well Name the two models, give the defining property of each in one sentence (order-independence versus positional forwarding), attach at least two languages to each, and mention the operators each model needs: exclusion and aliasing for flattening, insertion position and forwarding for chaining. The sharpest observation available is Scala's hybrid - chain semantics, but loud on collisions that were not declared as overrides.

  • Which operators does a flattening model need that a chaining model does not?
    Exclusion and aliasing. Because no unit is "nearer" than another, the composer must be able to drop one provider's method (exclusion) or keep both under different names (aliasing) - PHP spells these `insteadof` and `as`, Pharo spells them as trait algebra operators. A chaining model needs neither, because insertion position already picks a winner and the loser stays reachable by forwarding.
  • Ruby offers both `include` and `prepend`. What does the difference buy?
    `include` puts the module behind the class in the ancestor chain, so the class's own definition wins and the module supplies only what the class lacks. `prepend` puts it in front, so the module's method runs first and can call `super` to reach the class's original - that is how Ruby writes decorators such as instrumentation or memoization without editing the class. The choice is precisely a choice of position in the chain.
  • Why can a flattening system not express a decorator that wraps the previous implementation?
    Because flattening is defined as order-independent, there is no "previous implementation" to name - the composed class is equivalent to one in which the methods were written directly, so nothing sits behind anything else. Decoration therefore has to be expressed with an explicit wrapper object that holds the wrapped instance, or by aliasing the original to a second name and calling it from the replacement.

Flattening is photocopying pages into one binder - if two pages carry the same page number, someone must decide before it goes to print. Chain insertion is stacking transparencies - each layer covers what is under it and can leave a window through to it, so the stacking order is the design.

saying these in an interview costs you the question

  • Using trait and mixin as synonyms without noting that one is defined by order-independence and the other by position in the lookup chain.
  • Claiming composition order never matters for traits - true for the flattening model, false for Scala, whose whole stacking idiom depends on order.
  • Assuming a name collision is always an error - it is in PHP, but is silently resolved by position in Ruby and only reported at the call site in Rust.
  • Thinking `include` in Ruby overrides the class's own method; it does not, that is what `prepend` is for.
  • Believing linearization is what makes something a trait, when the original trait definition explicitly rules ordering out.

context

open as a page

Can a reusable unit of behaviour own instance state of its own? Compare how several named languages answer, and say what goes wrong in each.

level: seniorimportance: should knowfreq 34%

basics

~20 s

Answers split three ways. Scala traits may declare fields, with a linearization-ordered initialization hazard. Ruby modules assign undeclared instance variables that silently collide. Rust, Java, C# and Swift forbid stored state and make the unit demand an accessor instead.

open as a page