Scala 3 — Book

Вариантность

Language

Вариантность (variance) параметра типа управляет подтипом параметризованных типов (таких, как классы или трейты).

Чтобы объяснить вариантность, давайте рассмотрим следующие определения типов:

trait Item { def productNumber: String }
trait Buyable extends Item { def price: Int }
trait Book extends Buyable { def isbn: String }

Предположим также следующие параметризованные типы:

// пример инвариантного типа
trait Pipeline[T] {
  def process(t: T): T
}

// пример ковариантного типа
trait Producer[+T] {
  def make: T
}

// пример контрвариантного типа
trait Consumer[-T] {
  def take(t: T): Unit
}
// пример инвариантного типа
trait Pipeline[T]:
  def process(t: T): T

// пример ковариантного типа
trait Producer[+T]:
  def make: T

// пример контрвариантного типа
trait Consumer[-T]:
  def take(t: T): Unit

В целом существует три режима вариантности (variance):

  • инвариант (invariant) — значение по умолчанию, написанное как Pipeline[T]
  • ковариантный (covariant) — помечен знаком +, например Producer[+T]
  • контравариантный (contravariant) — помечен знаком -, как в Consumer[-T]

Подробнее рассмотрим, что означает и как используется эта аннотация.

Инвариантные типы

По умолчанию такие типы, как Pipeline, инвариантны в своем аргументе типа (в данном случае T). Это означает, что такие типы, как Pipeline[Item], Pipeline[Buyable] и Pipeline[Book], не являются подтипами друг друга.

И это правильно! Предположим, что следующий метод использует два значения (b1, b2) типа Pipeline[Buyable] и передает свой аргумент b методу process при его вызове на b1 и b2:

def oneOf(
  p1: Pipeline[Buyable],
  p2: Pipeline[Buyable],
  b: Buyable
): Buyable = {
  val b1 = p1.process(b)
  val b2 = p2.process(b)
  if (b1.price < b2.price) b1 else b2
 }
def oneOf(
  p1: Pipeline[Buyable],
  p2: Pipeline[Buyable],
  b: Buyable
): Buyable =
  val b1 = p1.process(b)
  val b2 = p2.process(b)
  if b1.price < b2.price then b1 else b2

Теперь вспомните, что у нас есть следующие отношения подтипов между нашими типами:

Book <: Buyable <: Item

Мы не можем передать Pipeline[Book] методу oneOf, потому что в реализации oneOf мы вызываем p1 и p2 со значением типа Buyable. Pipeline[Book] ожидает Book, что потенциально может вызвать ошибку времени выполнения.

Мы не можем передать Pipeline[Item], потому что вызов process обещает вернуть Item; однако мы должны вернуть Buyable.

Почему Инвариант?

На самом деле тип Pipeline должен быть инвариантным, так как он использует свой параметр типа T и в качестве аргумента, и в качестве типа возвращаемого значения. По той же причине некоторые типы в библиотеке коллекций Scala, такие как Array или Set, также являются инвариантными.

Ковариантные типы

В отличие от Pipeline, который является инвариантным, тип Producer помечается как ковариантный (covariant) путем добавления к параметру типа префикса +. Это допустимо, так как параметр типа используется только в качестве типа возвращаемого значения.

Пометка типа как ковариантного означает, что мы можем передать (или вернуть) Producer[Book] там, где ожидается Producer[Buyable]. И на самом деле, это разумно. Тип Producer[Buyable].make только обещает вернуть Buyable. Но для пользователей make, так же допустимо принять Book, который является подтипом Buyable, то есть это по крайней мере Buyable.

Это иллюстрируется следующим примером, где функция makeTwo ожидает Producer[Buyable]:

def makeTwo(p: Producer[Buyable]): Int =
  p.make.price + p.make.price

Допустимо передать в makeTwo производителя книг:

val bookProducer: Producer[Book] = ???
makeTwo(bookProducer)

Вызов price в рамках makeTwo по-прежнему действителен и для Book.

Ковариантные типы для неизменяемых контейнеров

Ковариантность чаще всего встречается при работе с неизменяемыми контейнерами, такими как List, Seq, Vector и т.д.

Например, List и Vector определяются приблизительно так:

class List[+A] ...
class Vector[+A] ...

Таким образом, можно использовать List[Book] там, где ожидается List[Buyable]. Это также интуитивно имеет смысл: если ожидается коллекция вещей, которые можно купить, то вполне допустимо получить коллекцию книг. В примере выше у книг есть дополнительный метод isbn, но дополнительные возможности можно игнорировать.

Контравариантные типы

В отличие от типа Producer, который помечен как ковариантный, тип Consumer помечен как контравариантный (contravariant) путем добавления к параметру типа префикса -. Это допустимо, так как параметр типа используется только в позиции аргумента.

Пометка его как контравариантного означает, что можно передать (или вернуть) Consumer[Item] там, где ожидается Consumer[Buyable]. То есть у нас есть отношение подтипа Consumer[Item] <: Consumer[Buyable]. Помните, что для типа Producer все было наоборот, и у нас был Producer[Buyable] <: Producer[Item].

И в самом деле, это разумно. Метод Consumer[Item].take принимает Item. Как вызывающий take, мы также можем предоставить Buyable, который будет с радостью принят Consumer[Item], поскольку Buyable — это подтип Item, то есть, по крайней мере, Item.

Контравариантные типы для потребителей

Контравариантные типы встречаются гораздо реже, чем ковариантные типы. Как и в нашем примере, вы можете думать о них как о «потребителях». Наиболее важным типом, помеченным как контравариантный, с которым можно столкнуться, является тип функций:

trait Function[-A, +B] {
  def apply(a: A): B
}
trait Function[-A, +B]:
  def apply(a: A): B

Тип аргумента A помечен как контравариантный A — он использует значения типа A. Тип результата B, напротив, помечен как ковариантный — он создает значения типа B.

Вот несколько примеров, иллюстрирующих отношения подтипов, вызванные аннотациями вариантности функций:

val f: Function[Buyable, Buyable] = b => b

// OK - допустимо вернуть Buyable там, где ожидается Item
val g: Function[Buyable, Item] = f

// OK - допустимо передать аргумент Book туда, где ожидается Buyable
val h: Function[Book, Buyable] = f

Резюме

В этом разделе были рассмотрены три различных вида вариантности:

  • Producers обычно ковариантны и помечают свой параметр типа со знаком +. Это справедливо и для неизменяемых коллекций.
  • Consumers обычно контравариантны и помечают свой параметр типа со знаком -.
  • Типы, которые являются одновременно производителями и потребителями, должны быть инвариантными и не требуют какой-либо маркировки для параметра своего типа. В эту категорию, в частности, попадают изменяемые коллекции, такие как Array.

Contributors to this page: