Scala Traits: Mixins, Interface Composition, and Self Types

TT

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:

scala
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 Alice

Mixing Multiple Traits

A class can mix in multiple traits using with:

scala
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:

scala
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 lost

Trait Fields

Traits can declare fields, both abstract and concrete:

scala
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:

scala
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 -> A

The 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:

scala
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:

scala
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)) // Unknown

The 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:

scala
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.