Что такое 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». Этот подход помогает строить масштабируемые и безопасные абстракции, особенно в функциональных системах.