layout | title | scala3 | partof | overview-name | type | description | language | num | previous-page | next-page |
---|---|---|---|---|---|---|---|---|---|---|
multipage-overview |
Классы типов |
true |
scala3-book |
Scala 3 — Book |
section |
В этой главе демонстрируется создание и использование классов типов. |
ru |
64 |
ca-given-imports |
ca-multiversal-equality |
Класс типов (type class) — это абстрактный параметризованный тип,
который позволяет добавлять новое поведение к любому закрытому типу данных без использования подтипов.
Если вы пришли с Java, то можно думать о классах типов как о чем-то вроде [java.util.Comparator[T]
][comparator].
В статье "Type Classes as Objects and Implicits" (2010 г.) обсуждаются основные идеи, лежащие в основе классов типов в Scala. Несмотря на то, что в статье используется более старая версия Scala, идеи актуальны и по сей день.
Этот стиль программирования полезен во многих случаях, например:
- выражение того, как тип, которым вы не владеете, например, из стандартной или сторонней библиотеки, соответствует такому поведению
- добавление поведения к нескольким типам без введения отношений подтипов между этими типами (например, когда один расширяет другой)
Классы типов — это трейты с одним или несколькими параметрами,
реализации которых предоставляются в виде экземпляров given
в Scala 3 или implicit
значений в Scala 2.
Например, Show
- хорошо известный класс типов в Haskell, и в следующем коде показан один из способов его реализации в Scala.
Если предположить, что классы Scala не содержат метода toString
, то можно определить класс типов Show
,
чтобы добавить это поведение к любому типу, который вы хотите преобразовать в пользовательскую строку.
Первым шагом в создании класса типов является объявление параметризованного trait, содержащего один или несколько абстрактных методов.
Поскольку Showable
содержит только один метод с именем show
, он записывается так:
{% tabs 'definition' class=tabs-scala-version %} {% tab 'Scala 2' %}
// класс типов
trait Showable[A] {
def show(a: A): String
}
{% endtab %} {% tab 'Scala 3' %}
// класс типов
trait Showable[A]:
extension (a: A) def show: String
{% endtab %} {% endtabs %}
Обратите внимание, что этот подход близок к обычному объектно-ориентированному подходу,
когда обычно trait Show
определяется следующим образом:
{% tabs 'trait' class=tabs-scala-version %} {% tab 'Scala 2' %}
// a trait
trait Show {
def show: String
}
{% endtab %} {% tab 'Scala 3' %}
// a trait
trait Show:
def show: String
{% endtab %} {% endtabs %}
Следует отметить несколько важных моментов:
- Классы типов, например,
Showable
принимают параметр типаA
, чтобы указать, для какого типа мы предоставляем реализациюshow
; в отличие от классических трейтов, наподобиеShow
. - Чтобы добавить функциональность
show
к определенному типуA
, классический трейт требует наследованияA extends Show
, в то время как для классов типов нам требуется реализацияShowable[A]
. - В Scala 3, чтобы разрешить один и тот же синтаксис вызова метода в обоих случаях
Showable
, который имитирует синтаксисShow
, мы определяемShowable.show
как метод расширения.
Следующий шаг — определить, какие классы Showable
должны работать в вашем приложении, а затем реализовать для них это поведение.
Например, для реализации Showable
следующего класса Person
:
{% tabs 'person' %} {% tab 'Scala 2 и 3' %}
case class Person(firstName: String, lastName: String)
{% endtab %} {% endtabs %}
необходимо определить одно каноническое значение типа Showable[Person]
, т.е. экземпляр Showable
для типа Person
,
как показано в следующем примере кода:
{% tabs 'instance' class=tabs-scala-version %} {% tab 'Scala 2' %}
implicit val showablePerson: Showable[Person] = new Showable[Person] {
def show(p: Person): String =
s"${p.firstName} ${p.lastName}"
}
{% endtab %} {% tab 'Scala 3' %}
given Showable[Person] with
extension (p: Person) def show: String =
s"${p.firstName} ${p.lastName}"
{% endtab %} {% endtabs %}
Теперь вы можете использовать этот класс типов следующим образом:
{% tabs 'usage' class=tabs-scala-version %} {% tab 'Scala 2' %}
val person = Person("John", "Doe")
println(showablePerson.show(person))
Обратите внимание, что на практике классы типов обычно используются со значениями, тип которых неизвестен,
в отличие от type Person
, как показано в следующем разделе.
{% endtab %} {% tab 'Scala 3' %}
val person = Person("John", "Doe")
println(person.show)
{% endtab %} {% endtabs %}
Опять же, если бы в Scala не было метода toString
, доступного для каждого класса, вы могли бы использовать эту технику,
чтобы добавить поведение Showable
к любому классу, который вы хотите преобразовать в String
.
Как и в случае с наследованием, вы можете определить методы, которые используют Showable
в качестве параметра типа:
{% tabs 'method' class=tabs-scala-version %} {% tab 'Scala 2' %}
def showAll[A](as: List[A])(implicit showable: Showable[A]): Unit =
as.foreach(a => println(showable.show(a)))
showAll(List(Person("Jane"), Person("Mary")))
{% endtab %} {% tab 'Scala 3' %}
def showAll[A: Showable](as: List[A]): Unit =
as.foreach(a => println(a.show))
showAll(List(Person("Jane"), Person("Mary")))
{% endtab %} {% endtabs %}
Обратите внимание: если вы хотите создать класс типов с несколькими методами, исходный синтаксис выглядит следующим образом:
{% tabs 'multiple-methods' class=tabs-scala-version %} {% tab 'Scala 2' %}
trait HasLegs[A] {
def walk(a: A): Unit
def run(a: A): Unit
}
{% endtab %} {% tab 'Scala 3' %}
trait HasLegs[A]:
extension (a: A)
def walk(): Unit
def run(): Unit
{% endtab %} {% endtabs %}
В качестве примера из реального мира, как классы типов используются в Scala 3,
см. обсуждение CanEqual
в [разделе Multiversal Equality][multiversal].
[typeclasses-chapter]: {% link _overviews/scala3-book/ca-type-classes.md %} [comparator]: https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html [multiversal]: {% link _overviews/scala3-book/ca-multiversal-equality.md %}