Как проверить ковариантное и контравариантное положение элемента в функции?

Это фрагмент кода из одной из прочитанных мной статей о контравариантности и ковариантности в scala. Однако мне не удается понять сообщение об ошибке, выданное компилятором scala "error: ковариантный тип A находится в контравариантной позиции в типе A значения pet2.

class Pets[+A](val pet:A) {
  def add(pet2: A): String = "done"
}

Насколько я понимаю, этот фрагмент кода состоит в том, что Pets является ковариантным и принимает объекты, которые являются подтипами A. Однако функция add принимает параметр только типа A. Ковариантность означает, что Pets могут принимать параметры типа A и его подтипов. Тогда как это должно вызывать ошибку. Откуда вообще возникает вопрос о контравариантности.

Любое объяснение приведенного выше сообщения об ошибке будет очень полезным. Спасибо


person Chaitanya Waikar    schedule 15.02.2018    source источник


Ответы (2)


TL; DR:

  • Ваш класс Pets может производить значения типа A, возвращая переменную-член pet, поэтому Pet[VeryGeneral] не может быть подтипом Pet[VerySpecial], потому что, когда он производит что-то VeryGeneral, он не может гарантировать, что он также является экземпляром VerySpecial. Следовательно, он не может быть контравариантным.

  • Ваш Pets класс может потреблять значения типа A, передавая их в качестве аргументов в add. Следовательно, Pet[VerySpecial] не может быть подтипом pet Pet[VeryGeneral], потому что он подавится любым вводом, отличным от VerySpecial. Следовательно, ваш класс не может быть ковариантным.

Остается только одна возможность: Pets должен быть неизменным в A.


Иллюстрация: Ковариация против контравариантности:

Я воспользуюсь этой возможностью, чтобы представить улучшенную и значительно более строгую версию этого комикса. Это иллюстрация концепций ковариации и контравариантности для языков программирования с аннотациями подтипов и вариаций на объекте объявления (очевидно, даже Java-разработчики сочли это достаточно поучительным, несмотря на то, что вопрос касался дисперсии использования сайта).

Во-первых, иллюстрация:

ковариация-контравариантность-комикс

Теперь более подробное описание с компилируемым кодом Scala.

Объяснение контравариантности (левая часть рисунка 1)

Рассмотрим следующую иерархию источников энергии, от очень общей до очень конкретной:

class EnergySource
class Vegetables extends EnergySource
class Bamboo extends Vegetables

Теперь рассмотрим черту Consumer[-A], которая имеет единственный consume(a: A)-метод:

trait Consumer[-A] {
  def consume(a: A): Unit
}

Давайте реализуем несколько примеров этой черты:

object Fire extends Consumer[EnergySource] {
  def consume(a: EnergySource): Unit = a match {
    case b: Bamboo => println("That's bamboo! Burn, bamboo!")
    case v: Vegetables => println("Water evaporates, vegetable burns.")
    case c: EnergySource => println("A generic energy source. It burns.")
  }
}

object GeneralistHerbivore extends Consumer[Vegetables] {
  def consume(a: Vegetables): Unit = a match {
    case b: Bamboo => println("Fresh bamboo shoots, delicious!")
    case v: Vegetables => println("Some vegetables, nice.")
  }
}

object Panda extends Consumer[Bamboo] {
  def consume(b: Bamboo): Unit = println("Bamboo! I eat nothing else!")
}

Итак, почему Consumer должен быть контравариантным в A? Давайте попробуем создать несколько разных источников энергии, а затем скормить их разным потребителям:

val oilBarrel = new EnergySource
val mixedVegetables = new Vegetables
val bamboo = new Bamboo

Fire.consume(bamboo)                // ok
Fire.consume(mixedVegetables)       // ok
Fire.consume(oilBarrel)             // ok

GeneralistHerbivore.consume(bamboo)           // ok
GeneralistHerbivore.consume(mixedVegetables)  // ok
// GeneralistHerbivore.consume(oilBarrel)     // No! Won't compile

Panda.consume(bamboo)               // ok
// Panda.consume(mixedVegetables)   // No! Might contain sth Panda is allergic to
// Panda.consume(oilBarrel)         // No! Pandas obviously cannot eat crude oil

Результат: Fire может потреблять все, что GeneralistHerbivore может потреблять, и, в свою очередь, GeneralistHerbivore может потреблять все, что Panda может съесть. Следовательно, до тех пор, пока мы заботимся только о способности потреблять источники энергии, Consumer[EnergySource] можно заменить там, где требуется Consumer[Vegetables], и Consumer[Vegetables], где требуется Consumer[Bamboo]. Следовательно, имеет смысл, что Consumer[EnergySource] <: Consumer[Vegetables] и Consumer[Vegetables] <: Consumer[Bamboo], даже если соотношение между параметрами типа прямо противоположное:

type >:>[B, A] = A <:< B

implicitly:          EnergySource  >:>          Vegetables
implicitly:          EnergySource                           >:>          Bamboo
implicitly:                                     Vegetables  >:>          Bamboo

implicitly: Consumer[EnergySource] <:< Consumer[Vegetables]
implicitly: Consumer[EnergySource]                          <:< Consumer[Bamboo]
implicitly:                            Consumer[Vegetables] <:< Consumer[Bamboo]

Объяснение ковариации (правая часть рисунка 1)

Определите иерархию продуктов:

class Entertainment
class Music extends Entertainment
class Metal extends Music // yes, it does, seriously^^

Определите типаж, который может создавать значения типа A:

trait Producer[+A] {
  def get: A
}

Определите различных «источников» / «производителей» разного уровня специализации:

object BrowseYoutube extends Producer[Entertainment] {
  def get: Entertainment = List(
    new Entertainment { override def toString = "Lolcats" },
    new Entertainment { override def toString = "Juggling Clowns" },
    new Music { override def toString = "Rick Astley" }
  )((System.currentTimeMillis % 3).toInt)
}

object RandomMusician extends Producer[Music] {
  def get: Music = List(
    new Music { override def toString = "...plays Mozart's Piano Sonata no. 11" },
    new Music { override def toString = "...plays BBF3 piano cover" }
  )((System.currentTimeMillis % 2).toInt)
}

object MetalBandMember extends Producer[Metal] {
  def get = new Metal { override def toString = "I" }
}

BrowseYoutube - это самый общий источник Entertainment: он может дать вам практически любой вид развлечения: видео с кошками, жонглирование клоунами или (случайно) немного музыки. Этот общий источник Entertainment представлен архетипическим шутом на Рисунке 1.

RandomMusician уже несколько более специализирован, по крайней мере, мы знаем, что этот объект производит музыку (хотя нет никаких ограничений для какого-либо определенного жанра).

Наконец, MetalBandMember чрезвычайно специализирован: метод get гарантированно возвращает только очень специфический тип Metal музыки.

Давайте попробуем получить различные виды Entertainment из этих трех объектов:

val entertainment1: Entertainment = BrowseYoutube.get   // ok
val entertainment2: Entertainment = RandomMusician.get  // ok
val entertainment3: Entertainment = MetalBandMember.get // ok

// val music1: Music = BrowseYoutube.get // No: could be cat videos!
val music2: Music = RandomMusician.get   // ok
val music3: Music = MetalBandMember.get  // ok

// val metal1: Entertainment = BrowseYoutube.get   // No, probably not even music
// val metal2: Entertainment = RandomMusician.get  // No, could be Mozart, could be Rick Astley
val metal3: Entertainment = MetalBandMember.get    // ok, because we get it from the specialist

Мы видим, что все три Producer[Entertainment], Producer[Music] и Producer[Metal] могут давать какой-то Entertainment. Мы видим, что только Producer[Music] и Producer[Metal] гарантированно произведут Music. Наконец, мы видим, что только чрезвычайно специализированный Producer[Metal] гарантированно производит Metal и ничего больше. Следовательно, Producer[Music] и Producer[Metal] можно заменить на Producer[Entertainment]. Producer[Metal] можно заменить на Producer[Music]. В общем, производитель более конкретного вида продукции может быть заменен менее специализированным производителем:

implicitly:          Metal  <:<          Music
implicitly:          Metal                      <:<          Entertainment
implicitly:                              Music  <:<          Entertainment

implicitly: Producer[Metal] <:< Producer[Music]
implicitly: Producer[Metal]                     <:< Producer[Entertainment]
implicitly:                     Producer[Music] <:< Producer[Entertainment]

Отношения подтипов между продуктами такие же, как отношения подтипов между производителями продуктов. Вот что означает ковариация.


Ссылки по теме

  1. Аналогичное обсуждение ? extends A и ? super B в Java 8: Статическая функция Java 8 Comparator comparing()

  2. Классический вопрос «какие параметры типа для flatMap в моей собственной реализации Either»: Тип L появляется в контравариантной позиции в Either[L, R]

person Andrey Tyukin    schedule 19.02.2018
comment
После прочтения вашего примера, да, это очень ясно и понятно, поскольку в сети есть тысячи блогов, подобных этому примеру с яблоками и бананами. Единственное, чего я не понимаю: \ n Почему вы выбрали контравариантный пример как трейт Consumer [-A] {def consumer (a: A): Unit} и ковариантный пример как: trait Producer [+ A] {def get: A} - person PainPoints; 18.11.2019

Класс Pets ковариантен в своем типе A (потому что он отмечен как + A), но вы используете его в позиции контравариантности. Это связано с тем, что, если вы взглянете на черту Function в Scala, вы увидите, что тип входного параметра контравариантен, а тип возвращаемого значения - ковариантен. Каждая функция контравариантна по типу ввода и ковариантна по типу возвращаемого значения.

Например, функция, принимающая один аргумент, имеет следующее определение:

trait Function1[-T1, +R]

Дело в том, что для того, чтобы функция S была подтипом функции F, она должна «требовать (такое же или) меньше и предоставлять (такое же или) больше». Это также известно как принцип замещения Лискова. На практике это означает, что свойство Function должно быть контравариантным на входе и ковариантным на выходе. Будучи контравариантным во вводе, он требует «такой же или меньше», потому что он принимает либо T1, либо любой из его супертипов (здесь «меньше» означает «супертип», потому что мы ослабляем ограничение, например, от Fruit к Food). Кроме того, будучи ковариантным в своем возвращаемом типе, он требует «то же самое или более», что означает, что он может возвращать R или что-то более конкретное, чем это (здесь «больше» означает «подтип», потому что мы добавляем больше информации, например, из Fruit в Яблоко).

Но почему? Почему не наоборот? Вот пример, который, надеюсь, объяснит это более интуитивно - представьте две конкретные функции, одна из которых является подтипом другой:

val f: Fruit => Fruit
val s: Food => Apple

Функция s является допустимым подтипом для функции f, потому что она требует меньше (мы «теряем» информацию от Fruit к Food) и предоставляет больше (мы «получаем» информацию от Fruit к Apple). Обратите внимание, что s имеет тип ввода, который является супертипом типа ввода f (контравариантность), и имеет тип возвращаемого значения, являющийся подтипом типа возврата f (ковариация). А теперь представим кусок кода, который использует такие функции:

def someMethod(fun: Fruit => Fruit) = // some implementation

Оба someMethod(f) и someMethod(s) являются допустимыми вызовами. Метод someMethod использует fun внутри, чтобы приложить к нему фрукты и получить из них фрукты. Поскольку s является подтипом f, это означает, что мы можем предоставить Food => Apple, который будет служить отличным экземпляром fun. Код внутри someMethod в какой-то момент будет кормить fun фруктами, и это нормально, потому что fun принимает пищу, а фрукты . С другой стороны, fun, имеющий Apple в качестве типа возвращаемого значения, тоже нормально, потому что fun должен возвращать фрукты, и, возвращая яблоки, он соответствует этому контракту.

Надеюсь, мне удалось немного прояснить это, не стесняйтесь задавать дополнительные вопросы.

person slouc    schedule 15.02.2018
comment
Как следует изменить эту функцию def add (pet2: A): String, чтобы удалить ошибку компиляции - person Chaitanya Waikar; 15.02.2018
comment
@ChaitanyaWaikar Зависит от того, что вы пытаетесь сделать ... Я всегда предпочел бы отойти от дисперсии и определить class Pets[A], но если это не вариант, вы можете обойти его с помощью def add[B >: A](pet2: B): String = "done". - person slouc; 15.02.2018