Skip to content
Traits
step 1/7

Reading — step 1 of 7

Learn

~3 min readClasses and Traits

Traits are Scala's solution to multiple inheritance — a unit of behavior + state that classes can mix into themselves with with. Programming in Scala calls them "the most distinctive feature of Scala".

Defining a trait

trait Greetable {
  def name: String              // abstract — concrete classes must define
  def greet: String = s"Hi, I'm $name"
}

class Person(val name: String) extends Greetable

val ada = new Person("Ada")
println(ada.greet)            // "Hi, I'm Ada"

Traits can have:

  • Abstract methods/values (no body — implementer must provide)
  • Concrete methods/values (default implementation)
  • Fields
  • Type members

Multiple traits with with

trait Loggable { def log(msg: String): Unit = println(s"[LOG] $msg") }
trait Serializable { def toJson: String }

class User(val name: String) extends Greetable with Loggable with Serializable {
  def toJson: String = s"""{"name":"$name"}"""
}

One extends for the first base (class or trait), then with for additional traits.

The diamond problem — solved by linearization

If two traits define the same method, Scala uses trait linearization:

trait A { def hi: String = "A" }
trait B extends A { override def hi: String = "B" }
trait C extends A { override def hi: String = "C" }

class D extends B with C

new D().hi             // "C" — last trait wins

The linearization order is: D, C, B, A. The rightmost trait wins for conflicting methods. super walks the linearization order.

Stackable modifications

A powerful trait pattern — building behavior in layers:

abstract class IntQueue {
  def get(): Int
  def put(x: Int): Unit
}

class BasicIntQueue extends IntQueue {
  private val buf = collection.mutable.ArrayBuffer.empty[Int]
  def get(): Int = buf.remove(0)
  def put(x: Int): Unit = buf += x
}

trait Doubling extends IntQueue {
  abstract override def put(x: Int): Unit = super.put(x * 2)
}

trait Filtering extends IntQueue {
  abstract override def put(x: Int): Unit = if (x >= 0) super.put(x)
}

val q = new BasicIntQueue with Filtering with Doubling
q.put(-5)                // filtered out
q.put(3)                  // doubled to 6

Each trait modifies behavior using abstract override + super. Traits compose like middleware.

Self-types

Declare what a trait REQUIRES (without inheriting):

trait Logger { def log(msg: String): Unit }

trait UserService { self: Logger =>
  def create(name: String): Unit = log(s"creating $name")
}

class MyService extends UserService with Logger {
  def log(msg: String): Unit = println(msg)
}

Like a structural "requires" without inheritance.

When trait vs abstract class?

  • Trait — multiple implementation/composition; pure code reuse without single-inheritance constraints
  • Abstract class — single inheritance; needs constructor parameters (Scala 2); Java interop

In Scala 3, traits CAN have constructor parameters too — narrowing the gap further.

Common mistakes

  • Misusing with for the first traitextends is needed first.
  • Conflicting methods without override — the compiler errors. Use override def and decide which trait's behavior wins.
  • Diamond inheritance confusion — debug with linearization. Rightmost-first.
  • Putting heavy state in traits — each trait's val is part of the linearization; surprises if you assume static-like behavior.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…