Introduction
Mixing reusable behaviour into a class from several independent sources is a problem every language answers differently. Python has multiple inheritance, TypeScript has mixins, Java has interfaces with default methods. Scala's trait covers what all three do, and it adds the part the others leave fuzzy: an explicit, deterministic rule for resolving conflicts.
Python: multiple inheritance, MRO complexity
Python allows mixing in as many base classes as you like, and resolves conflicts via C3 linearization (the Method Resolution Order). It works, but it's notoriously easy to construct a diamond where the actual resolved order surprises even an experienced reader, and the classic "fragile base class" problem is a well-known Python pitfall: change an unrelated method in a base class, and a subclass three levels down breaks in a way its own code gives no hint of.
class Persistable:
def save(self):
print("actually persisting")
class Logged(Persistable):
def save(self):
print("logging")
super().save()
class Validated(Persistable):
def save(self):
self.validate()
super().save()
def validate(self):
print("validating")
pass
class User(Logged, Validated):
pass
# Which order do Logged and Validated actually run in? You have to
# mentally compute User.__mro__ to be sure: it's hardly visible at
# the User class declaration itself.TypeScript: mixins as a workaround
TypeScript has no native multiple-inheritance-like construct at all. Mixins are a documented pattern, built out of functions that take a class and return an extended class, not a language feature. It works, but it's noticeably more ceremony than declaring an interface, and conflict resolution between two mixins touching the same method is whatever JavaScript's prototype chain happens to produce, not something the type system reasons about for you.
type Constructor<T = {}> = new (...args: any[]) => T;
function Logged<TBase extends Constructor>(Base: TBase) {
return class extends Base {
save() {
console.log("saving");
// @ts-ignore: TS has no reliable way to know Base has .save()
super.save();
}
};
}
class User { save() { /* ... */ } }
class LoggedUser extends Logged(User) {}
// Stacking a second mixin means nesting another function call, and the
// TypeScript compiler mostly has to trust the pattern, not verify it.Java: default methods, no state
Java interfaces can carry default method implementations since Java 8, but interfaces still can't hold instance state (fields). Any behavior that needs to remember something between calls, has to fall back to composition or abstract classes, which means you often can't mix in the full behavior you actually want from an interface alone.
interface RateLimited {
// No field allowed here: interfaces can't hold state.
// int requestCount = 0; // this must be defined as a constant, not mutable state.
default boolean allow() {
// Nowhere to track "how many requests so far" without
// pushing the actual counter into every implementing class.
throw new UnsupportedOperationException("needs external state");
}
}Scala: stateful, composable, deterministically ordered
A Scala trait holds abstract members, real implementations and mutable state, and several traits mix into one class with the with keyword. When two mixed-in traits implement the same method and both call super, the order is not ambiguous. It comes from linearization, a specified algorithm that reads the trait list right to left.
trait Saveable:
def save(): Unit = println("actually persisting")
trait Logged extends Saveable:
override def save(): Unit =
println("logging")
super.save()
trait Validated extends Saveable:
override def save(): Unit =
println("validating")
super.save()
class User extends Saveable, Logged, Validated:
override def save(): Unit = super.save()
(new User).save()
// Prints:
// "validating"
// "logging"
// "actually persisting"
// Follows the chain Validated -> Logged -> Saveable
// This order is specified language behavior, not something you
// compute by tracing prototype chains or __mro__.And unlike Java interfaces, a trait can carry actual mutable fields: a rate limiter's request counter, for instance, lives directly in the trait that defines the rate-limiting behavior, not pushed out to every class that mixes it in:
trait RateLimited(maxPerMinute: Int):
private var count = 0
def allow(): Boolean =
count += 1
count <= maxPerMinutePredictable composition matters more when you didn't write the class
Diamond-shaped composition trips up agents working in an existing codebase. They can read each trait, mixin or base class on its own, but predicting how a class combining several of them actually behaves means running the resolution algorithm: computing an MRO in Python, or tracing a mixin chain by hand in TypeScript.
Scala's linearization is a fixed rule, right to left through the trait list, and a model can apply that rule mechanically. Simulating Python's C3 algorithm, or reverse-engineering what a hand-rolled TypeScript mixin does at runtime, it gets wrong far more often.
