Reading — step 1 of 7
Learn
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
withfor the first trait —extendsis needed first. - Conflicting methods without override — the compiler errors. Use
override defand 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…