Explain the process and benefits of 'Self-types' in Scala.
Instruction: Describe what self-types are and provide an example to illustrate their benefits in Scala applications.
Updated
A self-type narrows the type of this within a trait and requires each concrete instance using that trait to satisfy the stated type. It provides access to the required members without making the trait itself a subtype of the required trait.
trait Logger { def log(message: String): Unit }
trait Service { self: Logger =>
def record(message: String): Unit = log(s"service:$message")
}
class BufferLogger extends Logger {
private val entries = scala.collection.mutable.ArrayBuffer.empty[String]
def log(message: String): Unit = { entries += message; () }
def messages: List[String] = entries.toList
}
class RecordedService extends BufferLogger with Service
@main def selfTypeExample(): Unit = {
val service = new RecordedService
service.record("ready")
assert(service.messages == List("service:ready"))
}
The superclass already implements Logger, so no extra literal with Logger is required on RecordedService. Instantiating Service without satisfying its self-type fails. A reference whose static type is only Service is not automatically assignable to Logger; the self-type constrains composition rather than declaring public inheritance. Self-types
This can express dependencies among traits while allowing different implementations in tests. It does not build a dependency graph, allocate dependencies or guarantee safe initialization. The required behavior exists on the same object. Constructor injection is often clearer when a service should hold a separate collaborator. The buffer example is single-threaded and only records strings; no logging delivery, concurrency or security guarantees are implied.