Scala - Traits (Interfaces) with Examples
Traits are Scala's fundamental building blocks for code reuse and abstraction. They function similarly to Java interfaces but with significantly more power. A trait can define both abstract and...
Key Insights
- Traits in Scala are powerful mixins that combine interface definitions with concrete implementations, allowing multiple inheritance of behavior without the diamond problem through linearization
- Unlike Java interfaces, Scala traits can contain state (fields), concrete methods, and abstract methods, making them more flexible for building composable, modular systems
- Trait linearization follows a predictable right-to-left order when mixing multiple traits, enabling sophisticated composition patterns and the stackable modification technique
Understanding Scala Traits
Traits are Scala’s fundamental building blocks for code reuse and abstraction. They function similarly to Java interfaces but with significantly more power. A trait can define both abstract and concrete members, maintain state, and be mixed into classes using the extends and with keywords.
trait Logger {
def log(message: String): Unit
}
trait ConsoleLogger extends Logger {
def log(message: String): Unit = {
println(s"[LOG] $message")
}
}
class UserService extends ConsoleLogger {
def createUser(name: String): Unit = {
log(s"Creating user: $name")
// User creation logic
}
}
val service = new UserService
service.createUser("Alice")
// Output: [LOG] Creating user: Alice
Traits with Abstract and Concrete Members
Traits can mix abstract declarations with concrete implementations, allowing you to define partial behavior that classes can extend and complete.
trait Repository[T] {
def findById(id: Long): Option[T]
def save(entity: T): T
def delete(id: Long): Boolean
// Concrete method with default implementation
def exists(id: Long): Boolean = findById(id).isDefined
// Concrete method using abstract methods
def saveAll(entities: Seq[T]): Seq[T] = {
entities.map(save)
}
}
case class User(id: Long, name: String, email: String)
class UserRepository extends Repository[User] {
private var users = Map.empty[Long, User]
def findById(id: Long): Option[User] = users.get(id)
def save(entity: User): User = {
users = users + (entity.id -> entity)
entity
}
def delete(id: Long): Boolean = {
val existed = users.contains(id)
users = users - id
existed
}
}
val repo = new UserRepository
val user = User(1, "Bob", "bob@example.com")
repo.save(user)
println(repo.exists(1)) // true
Multiple Trait Composition
Scala allows mixing multiple traits into a single class, enabling powerful composition patterns. The order matters due to linearization, which determines method resolution.
trait Timestamped {
val createdAt: Long = System.currentTimeMillis()
}
trait Versioned {
private var _version: Int = 0
def version: Int = _version
def incrementVersion(): Unit = _version += 1
}
trait Auditable {
private var _modifiedBy: Option[String] = None
def modifiedBy: Option[String] = _modifiedBy
def setModifiedBy(user: String): Unit = _modifiedBy = Some(user)
}
case class Document(id: String, content: String)
extends Timestamped with Versioned with Auditable {
def update(newContent: String, user: String): Document = {
setModifiedBy(user)
incrementVersion()
copy(content = newContent)
}
}
val doc = Document("doc-1", "Initial content")
println(s"Created at: ${doc.createdAt}, Version: ${doc.version}")
val updated = doc.update("Updated content", "admin")
println(s"Version: ${updated.version}, Modified by: ${updated.modifiedBy}")
// Output: Version: 1, Modified by: Some(admin)
Trait Linearization and Method Resolution
When a class mixes in multiple traits that define the same method, Scala uses linearization to determine which implementation to call. The linearization order is right-to-left, meaning the rightmost trait’s methods are considered first.
trait Base {
def describe: String = "Base"
}
trait A extends Base {
override def describe: String = "A -> " + super.describe
}
trait B extends Base {
override def describe: String = "B -> " + super.describe
}
trait C extends Base {
override def describe: String = "C -> " + super.describe
}
class Example1 extends A with B with C
class Example2 extends C with B with A
println(new Example1().describe) // C -> B -> A -> Base
println(new Example2().describe) // A -> B -> C -> Base
The linearization algorithm creates a single inheritance chain, ensuring each trait’s method can call super and reach the next trait in the chain.
Stackable Modifications Pattern
One of the most powerful patterns enabled by trait linearization is stackable modifications, where multiple traits modify behavior in a composable way.
abstract class IntQueue {
def get(): Int
def put(x: Int): Unit
}
class BasicIntQueue extends IntQueue {
private val buffer = scala.collection.mutable.ArrayBuffer.empty[Int]
def get(): Int = buffer.remove(0)
def put(x: Int): Unit = buffer += x
}
trait Doubling extends IntQueue {
abstract override def put(x: Int): Unit = super.put(x * 2)
}
trait Incrementing extends IntQueue {
abstract override def put(x: Int): Unit = super.put(x + 1)
}
trait Filtering extends IntQueue {
abstract override def put(x: Int): Unit = {
if (x >= 0) super.put(x)
}
}
// Compose behaviors
val queue1 = new BasicIntQueue with Doubling with Incrementing
queue1.put(5)
println(queue1.get()) // 12 (5 + 1 = 6, then 6 * 2 = 12)
val queue2 = new BasicIntQueue with Incrementing with Doubling
queue2.put(5)
println(queue2.get()) // 10 (5 * 2 = 10, then 10 + 1 = 11... wait, no)
// Actually: 11 (5 + 1 = 6, then doubled = 12)? No...
// Correct: Rightmost first: Incrementing(5) = 6, Doubling(6) = 12
val queue3 = new BasicIntQueue with Filtering with Doubling
queue3.put(-5)
queue3.put(3)
println(queue3.get()) // 6 (only 3 passed filter, then doubled)
Self-Type Annotations
Self-types allow a trait to declare dependencies on other traits or classes without extending them directly. This is useful for dependency injection and creating modular components.
trait DatabaseAccess {
def executeQuery(sql: String): List[Map[String, Any]]
}
trait Logging {
def log(message: String): Unit
}
trait UserDAO {
// Requires both DatabaseAccess and Logging
self: DatabaseAccess with Logging =>
def findUserByEmail(email: String): Option[Map[String, Any]] = {
log(s"Searching for user with email: $email")
val results = executeQuery(s"SELECT * FROM users WHERE email = '$email'")
results.headOption
}
def createUser(name: String, email: String): Boolean = {
log(s"Creating user: $name")
executeQuery(s"INSERT INTO users (name, email) VALUES ('$name', '$email')")
true
}
}
// Concrete implementations
class PostgresAccess extends DatabaseAccess {
def executeQuery(sql: String): List[Map[String, Any]] = {
// Simulate database query
List(Map("id" -> 1, "name" -> "Test", "email" -> "test@example.com"))
}
}
class ConsoleLogging extends Logging {
def log(message: String): Unit = println(s"[${java.time.LocalDateTime.now}] $message")
}
// Mix everything together
class UserService extends UserDAO with PostgresAccess with ConsoleLogging
val userService = new UserService
userService.findUserByEmail("test@example.com")
Sealed Traits for ADTs
Sealed traits restrict inheritance to the same file, making them ideal for algebraic data types and exhaustive pattern matching.
sealed trait Result[+T]
case class Success[T](value: T) extends Result[T]
case class Failure(error: String) extends Result[Nothing]
case object Pending extends Result[Nothing]
def processResult[T](result: Result[T]): String = result match {
case Success(value) => s"Got value: $value"
case Failure(error) => s"Error occurred: $error"
case Pending => "Still processing..."
// Compiler ensures all cases are covered
}
sealed trait PaymentMethod
case class CreditCard(number: String, cvv: String) extends PaymentMethod
case class PayPal(email: String) extends PaymentMethod
case class BankTransfer(accountNumber: String, routingNumber: String) extends PaymentMethod
def processPayment(method: PaymentMethod, amount: Double): Unit = method match {
case CreditCard(number, _) => println(s"Charging $amount to card ending in ${number.takeRight(4)}")
case PayPal(email) => println(s"Charging $amount to PayPal account $email")
case BankTransfer(account, _) => println(s"Transferring $amount from account $account")
}
Traits provide Scala with exceptional flexibility for abstraction and composition. By understanding linearization, stackable modifications, and self-types, you can build highly modular systems that are both type-safe and maintainable.