Ковариантность типов

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

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

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

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

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

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

По умолчанию такие типы, как 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 then b1 else b2

Напомним, что отношения подтипов между типами следующие: Book <: Buyable <: Item.

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

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

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

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

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

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

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

Тип аргумента 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

Резюме

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


Ссылки: