Что такое typeclass и как её реализовать?

В Scala typeclass — это паттерн проектирования, который позволяет добавлять поведение к типам без изменения их исходного кода, не используя наследование. Это один из ключевых инструментов ад-хок полиморфизма, особенно активно применяемый в функциональном программировании и в библиотеках типа Cats, Scalaz и ZIO.

Typeclass-подход вдохновлён Haskell, но в Scala реализуется с помощью комбинации параметров типа, имплиситных значений и параметров, абстрактных типов и implicit resolution.

Что такое typeclass

Typeclass в Scala — это трейt с параметром типа, который описывает набор операций, доступных для типа T, при наличии его реализации (инстанса) typeclass’а.

Пример: мы хотим, чтобы типы, для которых есть возможность превратить их в строку, могли использовать общий метод toStr.

1. Определение typeclass

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

Это интерфейс typeclass’а. Он говорит: «Если у типа T есть инстанс Show, то мы можем вызвать show и получить String».

2. Инстансы typeclass

Реализация для определённых типов:

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"String: $value"
}

Эти инстансы будут найдены компилятором автоматически при наличии implicit в зоне видимости.

3. Использование typeclass

def printWithShow\[T\](value: T)(implicit s: Show\[T\]): Unit = {
println(s.show(value))
}

Вызов:

printWithShow(123) // Int(123)
printWithShow("hello") // String: hello

Компилятор ищет подходящий Show[T] в имплиситной области видимости.

4. Typeclass как контекстное ограничение

Более идиоматичный способ — через контекстные bounds:

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

[T: Show] — это сокращение для [T](implicit ev: Show[T]).

5. Расширение функциональности через имплиситные классы

Вы можете "научить" тип T использовать методы typeclass’а напрямую:

implicit class ShowSyntax\[T\](value: T) {
def show(implicit s: Show\[T\]): String = s.show(value)
}
println(42.show) // Int(42)
println("Scala".show) // String: Scala

6. Typeclass с несколькими методами

trait Eq\[T\] {
def eqv(a: T, b: T): Boolean
}

Пример инстанса:

implicit val intEq: Eq\[Int\] = new Eq\[Int\] {
def eqv(a: Int, b: Int): Boolean = a == b
}

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

def areEqual\[T: Eq\](a: T, b: T): Boolean = {
implicitly\[Eq\[T\]\].eqv(a, b)
}

7. Композиция typeclass'ов

Можно создавать обобщённые методы, которые требуют несколько typeclass’ов одновременно:

def compareAndShow\[T: Show: Eq\](a: T, b: T): String = {
val shown = implicitly\[Show\[T\]\]
val eq = implicitly\[Eq\[T\]\]
if (eq.eqv(a, b)) s"${shown.show(a)} equals ${shown.show(b)}"
else s"${shown.show(a)} not equal to ${shown.show(b)}"
}

8. Автоматическое выведение с помощью given (в Scala 3)

В Scala 3 синтаксис упрощён:

trait Show\[T\]:
def show(value: T): String
given Show\[Int\] with
def show(value: Int): String = s"Int($value)"
def printWithShow\[T\](value: T)(using s: Show\[T\]) =
println(s.show(value))
printWithShow(123)

Также поддерживается синтаксис summon[Show[Int]] вместо implicitly.

9. Typeclass в библиотеках: пример Eq из Cats

import cats.Eq
import cats.syntax.eq._
import cats.instances.int._
val a = 42
val b = 43
val result = a === b // false, использует имплиситный Eq\[Int\]

10. Зачем использовать typeclass?

  • Позволяет расширять поведение чужих типов без изменения их кода.

  • Разделяет интерфейс и реализацию.

  • Обеспечивает ад-хок полиморфизм (в отличие от наследования).

  • Широко применяется в функциональных библиотеках.

  • Позволяет писать обобщённый, переиспользуемый код.

Typeclass-подход — это способ выразить: «Если у типа T есть поведение X, тогда мы можем делать Y». Этот подход помогает строить масштабируемые и безопасные абстракции, особенно в функциональных системах.