Что такое контекстные ограничения (context bounds)?

Контекстные ограничения (context bounds) в Scala — это синтаксическая конструкция, позволяющая задать требование, чтобы для типа, параметризующего класс или метод, существовал неявный экземпляр определённого типа класса (type class). Это более лаконичная форма записи неявных параметров.

Идея context bounds восходит к концепции type class, пришедшей из Haskell, где поведение типа определяется через соответствующий ему класс типов, а не через наследование.

Синтаксис

def example\[T: SomeTypeClass\](value: T): Result

Эта запись означает, что должен существовать неявный параметр типа SomeTypeClass[T]. За кулисами компилятор будет искать:

implicitly\[SomeTypeClass\[T\]\]

Расширенная запись (эквивалент)

Контекстное ограничение:

def foo\[T: Show\](x: T): String

Эквивалентно:

def foo\[T\](x: T)(implicit ev: Show\[T\]): String

Практический пример

Допустим, у нас есть type class Show, который знает, как превратить значение типа T в строку:

trait Show\[T\] {
def show(value: T): String
}

Теперь определим экземпляры для типов:

implicit val intShow: Show\[Int\] = new Show\[Int\] {
def show(value: Int): String = s"Int($value)"
}
implicit val stringShow: Show\[String\] = new Show\[String\] {
def show(value: String): String = s""""$value""""
}

Используем context bound:

def printValue\[T: Show\](value: T): Unit = {
val showInstance = implicitly\[Show\[T\]\]
println(showInstance.show(value))
}

Теперь можно вызвать:

printValue(123) // Int(123)
printValue("Scala") // "Scala"

implicitly и summon

Чтобы получить экземпляр класса типа внутри метода с context bound, используется implicitly[T] (в Scala 2 и 3) или summon[T] (в Scala 3).

def toStringUsingContextBound\[T: Show\](x: T): String = {
val ev = implicitly\[Show\[T\]\]
ev.show(x)
}

В Scala 3 (Dotty):

val ev = summon\[Show\[T\]\]

Совмещение нескольких context bounds

Можно наложить несколько context bound'ов:

def serializeAndCompare\[T: Show: Ordering\](x: T, y: T): Boolean = {
val show = implicitly\[Show\[T\]\]
val ordering = implicitly\[Ordering\[T\]\]
println(show.show(x))
ordering.gt(x, y)
}

Применение: Ordering, Numeric, Show, Encoder, Decoder, Eq

Наиболее распространённые type class'ы, для которых применяются context bounds:

  • Ordering[T] — для сравнения

  • Numeric[T] — для числовых операций

  • Show[T] — для приведения к строке (часто используется в cats)

  • Encoder[T], Decoder[T] — сериализация (например, Circe)

  • Eq[T] — сравнение на равенство (в функциональных библиотеках типа cats)

Пример сортировки:

def sortList\[T: Ordering\](list: List\[T\]): List\[T\] = {
list.sorted
}

Компилятор автоматически подставит Ordering[Int], Ordering[String] и т. д.

Контекстные ограничения в классах

class Wrapper\[T: Show\](value: T) {
def describe(): String = implicitly\[Show\[T\]\].show(value)
}

В Scala 3: context bounds и given/using

В Scala 3 implicit заменены на given и using, но context bounds остались:

def describe\[T: Show\](x: T): String = {
summon\[Show\[T\]\].show(x)
}

Или, если полностью явно:

def describe\[T\](x: T)(using s: Show\[T\]): String = s.show(x)

Преимущества context bounds

  • Сокращает синтаксис, делает сигнатуры методов компактнее

  • Явно выражает зависимость от type class, не перегружая читаемость

  • Поддерживает функциональный стиль и обобщённость

  • Позволяет отделить реализацию от интерфейса поведения

Ограничения context bounds

  • Работают только с type class'ами с одним параметром (Show[T], Ordering[T])

  • Для многопараметрических type class требуется явное implicit-параметр

Пример невозможного context bound (Scala не позволяет):

// НЕ сработает:
def foo\[T: MyTypeClass\[K\]\] = ???

Нужно писать:

def foo\[T\](implicit ev: MyTypeClass\[T, K\]) = ???

Контекстные ограничения в Scala — мощный инструмент для обобщённого и декларативного программирования. Они позволяют создавать универсальные абстракции с поддержкой неявных зависимостей и при этом сохранять чистоту сигнатур.