Scala Traits: Mixins, Interface Composition, and Self Types
Scala Traits: Mixins, Interface Composition, and Self Types
Traits are one of Scala's most powerful features. They are more capable than Java interfaces (they can contain concrete implementations) and safer than multiple inheritance (linearization prevents the diamond problem). This module covers everything you need to use traits effectively.
What Is a Trait?
A trait defines a type that can be mixed into classes. Traits can contain abstract members (without implementations), concrete members (with implementations), fields, and type members:
trait Greetable {
def name: String // abstract — must be implemented by the mixing class
def greet(): String = s"Hello, my name is $name" // concrete — already implemented
}
class Person(val name: String) extends Greetable
val p = new Person("Alice")
println(p.greet()) // Hello, my name is AliceMixing Multiple Traits
A class can mix in multiple traits using with:
trait Flyable {
def fly(): String = "I can fly!"
}
trait Swimmable {
def swim(): String = "I can swim!"
}
class Duck extends Flyable with Swimmable {
def quack(): String = "Quack!"
}
val duck = new Duck
println(duck.fly()) // I can fly!
println(duck.swim()) // I can swim!Abstract and Concrete Trait Members
Traits can mix abstract and concrete members freely:
trait Logger {
def log(message: String): Unit // abstract
def info(message: String): Unit = log(s"[INFO] $message") // concrete
def error(message: String): Unit = log(s"[ERROR] $message") // concrete
}
class ConsoleLogger extends Logger {
def log(message: String): Unit = println(message)
}
val logger = new ConsoleLogger
logger.info("Server started") // [INFO] Server started
logger.error("Connection lost") // [ERROR] Connection lostTrait Fields
Traits can declare fields, both abstract and concrete:
trait Config {
val maxRetries: Int // abstract
val timeout: Int = 30 // concrete with default
def describe: String = s"maxRetries=$maxRetries, timeout=$timeout"
}
class AppConfig extends Config {
val maxRetries: Int = 3
}Trait Linearization
When multiple traits define the same method, Scala uses linearization to determine which implementation runs. The order is: the class itself, then traits from right to left in the with chain:
trait A {
def hello(): String = "A"
}
trait B extends A {
override def hello(): String = s"B -> ${super.hello()}"
}
trait C extends A {
override def hello(): String = s"C -> ${super.hello()}"
}
class D extends A with B with C
val d = new D
println(d.hello()) // C -> B -> AThe linearization order for D is: D → C → B → A. So C.hello() is called first, which calls super.hello() — that goes to B, which calls super.hello() — that goes to A. This resolves the diamond problem cleanly.
Stackable Traits Pattern
Linearization enables the stackable traits pattern — building behavior by stacking traits:
abstract class Queue[T] {
def put(item: T): Unit
def get(): T
}
trait Doubling extends Queue[Int] {
abstract override def put(item: Int): Unit = super.put(item * 2)
}
trait Incrementing extends Queue[Int] {
abstract override def put(item: Int): Unit = super.put(item + 1)
}
class BasicQueue extends Queue[Int] {
private val buf = scala.collection.mutable.ArrayBuffer[Int]()
def put(item: Int): Unit = buf += item
def get(): Int = buf.remove(0)
}
val q = new BasicQueue with Doubling with Incrementing
q.put(5)
println(q.get()) // 12 — first incremented (5+1=6), then doubled (6*2=12)Self-Type Annotations
A self-type annotation declares that a trait requires another type to be present in the same class. This is used for dependency injection without inheritance:
trait UserRepository {
def findById(id: Int): Option[String]
}
trait UserService {
self: UserRepository => // self-type: requires UserRepository to be mixed in
def getUser(id: Int): String = findById(id).getOrElse("Unknown")
}
object App extends UserService with UserRepository {
def findById(id: Int): Option[String] = if (id == 1) Some("Alice") else None
}
println(App.getUser(1)) // Alice
println(App.getUser(99)) // UnknownThe self-type self: UserRepository => means "whoever mixes in UserService must also mix in UserRepository". If they don't, the compiler raises an error.
Sealed Traits
A sealed trait restricts which classes can extend it to those defined in the same file. This enables exhaustive pattern matching:
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(base: Double, height: Double) extends Shape
def area(shape: Shape): Double = shape match {
case Circle(r) => math.Pi * r * r
case Rectangle(w, h) => w * h
case Triangle(b, h) => 0.5 * b * h
}If you add a new case class that extends Shape and forget to handle it in the match, the compiler gives a warning. This is the foundation of algebraic data types (ADTs) in Scala.
Traits vs Abstract Classes
Use a trait when a class needs to mix in multiple behaviors. Use an abstract class when you need a constructor with parameters, when working with Java code, or when there is a clear "is-a" hierarchy. In most Scala code, traits are preferred.
Frequently Asked Questions
Q: Can a trait extend another trait in Scala?
Yes — traits can extend other traits, and this is common. When you write trait B extends A, B inherits A's abstract and concrete members. A class mixing in B gets both A and B's interface and implementations.
Q: What is the difference between a self-type and extending a trait?
Extending a trait with extends creates a subtype relationship — the extending class IS that trait. A self-type with self: Trait => creates a usage requirement without subtyping — the class REQUIRES that trait but doesn't declare itself as that type. Self-types are used for dependency injection because they avoid tight coupling while still getting type-safe access to the dependency.
Q: When does Scala's trait linearization matter in practice?
Linearization matters whenever two traits in the with chain define the same method. The most common practical case is when building stackable modifications — each trait adds behavior on top of super, and the order of the with list determines the stacking order. Understanding linearization prevents surprises when methods behave differently than expected.
Part of Scala Mastery Course — Module 7 of 22.
