Что такое self-type и где он полезен?

В Scala self-type (или self-type annotations) — это механизм, позволяющий указывать, какие другие типы обязательно должны быть смешаны с данным trait или class. Это не наследование, а ограничение компиляции, обеспечивающее, что нужная зависимость будет присутствовать. Self-type используется для внедрения зависимостей, разделения обязанностей, модульности, тестируемости и композиции поведения.

Синтаксис self-type

trait A {
def doA(): Unit
}
trait B {
self: A =>
def doB(): Unit = {
doA() // компилятор разрешает, потому что A гарантирован
println("Doing B")
}
}

Это означает: trait B можно использовать только с типом A. Иначе — ошибка компиляции.

Как это работает

Когда мы объявляем self: A =>, мы не наследуем A, но обязываем, чтобы любой, кто микширует B, также обеспечивал реализацию A.

Использование:

class MyClass extends A with B {
def doA(): Unit = println("Doing A")
}
val x = new MyClass
x.doB()
// Output:
// Doing A
// Doing B

Если попытаться сделать class MyClass extends B, не реализовав A, то:

Error: illegal inheritance; self-type MyClass does not conform to A

Отличие от наследования

trait B extends A

Это означает, что B наследует A, и может вызывать doA напрямую. Но это жесткое связывание: B всегда содержит A.

А self: A => — это мягкое требование, навязанное пользователю: "ты должен обеспечить A".

Где self-type полезен

1. Dependency injection (внедрение зависимостей)

trait Database {
def query(sql: String): List\[String\]
}
trait UserService {
self: Database =>
def getUsers(): List\[String\] = query("SELECT \* FROM users")
}
class ProdDatabase extends Database {
def query(sql: String) = List("user1", "user2")
}
class App extends ProdDatabase with UserService
val app = new App
app.getUsers()
// List("user1", "user2")

Таким образом, UserService ничего не знает про конкретную реализацию базы — только про то, что она будет.

2. Тестируемость

Можно подмешивать фиктивные реализации:

class MockDB extends Database {
def query(sql: String) = List("testUser")
}
class TestService extends MockDB with UserService
val test = new TestService
test.getUsers() // List("testUser")

3. Композиция поведения

Self-type позволяет строить сквозные абстракции, не полагаясь на иерархию наследования:

trait Auth {
def currentUser: String
}
trait Logging {
def log(msg: String): Unit = println(msg)
}
trait SecureLogging {
self: Auth with Logging =>
def secureLog(msg: String): Unit =
log(s"\[${currentUser}\] $msg")
}

Использование:

class App extends Auth with Logging with SecureLogging {
def currentUser = "admin"
def log(msg: String) = println(s"LOG: $msg")
}
new App().secureLog("Access granted")
// LOG: \[admin\] Access granted

4. Множественное связывание зависимостей

trait A
trait B
trait RequiresBoth {
self: A with B =>
}

Такой trait не может быть использован без одновременного наличия A и B. Это проверяется на этапе компиляции.

Использование с class

Self-type работает не только в trait, но и в class:

class MyClass {
self: Logging =>
def doSomething(): Unit = log("Hello")
}

Такой класс не может быть создан без Logging:

trait Logging {
def log(s: String): Unit
}
class MyLogger extends Logging {
def log(s: String): Unit = println(s)
}
val obj = new MyClass with MyLogger
obj.doSomething() // Hello

Self-type alias

Можно использовать другое имя для self-ссылки:

trait Service {
selfAlias: Logging =>
def run(): Unit = selfAlias.log("Running")
}

Имя selfAlias действует как this, но привязан к типу Logging.

Пример с Play Framework (или других DI)

Self-type идеально ложится в архитектуру, где модули не должны жёстко зависеть от друг друга. Например, можно собрать слои приложения из trait'ов:

trait Repos {
def getData(): String
}
trait Services {
self: Repos =>
def process(): String = s"Processed: ${getData()}"
}
trait Controllers {
self: Services =>
def run(): Unit = println(process())
}

И собрать приложение:

object App extends Repos with Services with Controllers {
def getData() = "123"
}
App.run()

Self-type + Cake Pattern

Механизм self-type лежит в основе cake pattern — архитектурного подхода в Scala, при котором зависимости собираются по слоям и "перемешиваются" через self-type. Это альтернатива DI-фреймворкам (например, Guice, Spring).

Отличие от this

Self-type — это типовая аннотация, используемая компилятором для проверки структуры. this — это указатель на текущий экземпляр. С self-type можно даже присвоить другое имя вместо this.

trait T {
self => // обычное имя по умолчанию
}

Можно так:

trait T {
my => // теперь вместо this можно писать my
}

Self-type — один из самых мощных инструментов в Scala, позволяющий строить гибкие, слабо связанные и типобезопасные архитектуры, особенно в крупных проектах и библиотеках.