Почему пример не компилируется, иначе как (со-, противо- и не-) дисперсия работает?

147

Исходя из этого вопроса , может кто-нибудь объяснить в Scala следующее:

class Slot[+T] (var some: T) { 
   //  DOES NOT COMPILE 
   //  "COVARIANT parameter in CONTRAVARIANT position"

}

Я понимаю различие между +Tи Tв объявлении типа (оно компилируется, если я использую T). Но тогда как на самом деле написать класс, который является ковариантным по параметру типа, не прибегая к созданию непараметризованной вещи ? Как я могу гарантировать, что следующее может быть создано только с экземпляром T?

class Slot[+T] (var some: Object){    
  def get() = { some.asInstanceOf[T] }
}

РЕДАКТИРОВАТЬ - теперь получил это до следующего:

abstract class _Slot[+T, V <: T] (var some: V) {
    def getT() = { some }
}

это все хорошо, но теперь у меня есть два параметра типа, где мне нужен только один. Я снова задам вопрос так:

Как я могу написать неизменный Slot класс, который является ковариантным в своем типе?

РЕДАКТИРОВАТЬ 2 : Дух! Я использовал varи нет val. Вот что я хотел:

class Slot[+T] (val some: T) { 
}
oxbow_lakes
источник
6
Потому varчто устанавливается, а valнет. Это та же самая причина, по которой неизменные коллекции scala являются ковариантными, а изменяемые - нет.
oxbow_lakes
Это может быть интересно в этом контексте: scala-lang.org/old/node/129
user573215

Ответы:

302

Как правило, параметр ковариантного типа - это параметр, который может изменяться в зависимости от подтипа класса (альтернативно, варьироваться в зависимости от подтипа, отсюда и префикс «co»). Более конкретно:

trait List[+A]

List[Int]является подтипом, List[AnyVal]потому что Intявляется подтипом AnyVal. Это означает, что вы можете предоставить случай, List[Int]когда List[AnyVal]ожидается значение типа . Это действительно очень интуитивный способ работы генериков, но оказывается, что он неэффективен (нарушает систему типов) при использовании в присутствии изменяемых данных. Вот почему дженерики инвариантны в Java. Краткий пример несостоятельности с использованием массивов Java (которые ошибочно ковариантны):

Object[] arr = new Integer[1];
arr[0] = "Hello, there!";

Мы просто присвоили значение типа Stringмассиву типов Integer[]. По причинам, которые должны быть очевидны, это плохие новости. Система типов Java фактически позволяет это во время компиляции. JVM «услужливо» сгенерирует ArrayStoreExceptionво время выполнения. Система типов Scala предотвращает эту проблему, потому что параметр типа в Arrayклассе является инвариантным ( [A]вместо объявления [+A]).

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

trait Function1[-P, +R] {
  def apply(p: P): R
}

Обратите внимание на аннотацию " - " для Pпараметра типа. Это объявление в целом означает, что Function1является контравариантным в Pи ковариантным в R. Таким образом, мы можем вывести следующие аксиомы:

T1' <: T1
T2 <: T2'
---------------------------------------- S-Fun
Function1[T1, T2] <: Function1[T1', T2']

Обратите внимание, что это T1'должен быть подтип (или тот же тип) T1, тогда как для T2и T2'. На английском языке это можно прочитать следующим образом:

Функция является подтипом другой функции B , если типа параметра А является супертипом типа параметра В то время как тип возвращаемого А является подтипом типа возвращаемого B .

Причина этого правила оставлена ​​читателю в качестве упражнения (подсказка: подумайте о разных случаях, когда функции имеют подтипы, как в примере с моим массивом выше).

С вашими новыми знаниями о со-и контравариантности вы сможете понять, почему следующий пример не скомпилируется:

trait List[+A] {
  def cons(hd: A): List[A]
}

Проблема в том, что Aона ковариантна, тогда как consфункция ожидает, что ее параметр типа будет инвариантным . Таким образом, Aменяется неправильное направление. Интересно, что мы могли бы решить эту проблему, сделав ее Listконтравариантной A, но тогда возвращаемый тип List[A]был бы недействительным, поскольку consфункция ожидает, что ее возвращаемый тип будет ковариантным .

Здесь есть только два варианта: а) сделать Aинвариант, потеряв приятные, интуитивно понятные свойства ковариации подтипирования, или б) добавить параметр локального типа в consметод, который определяет Aкак нижнюю границу:

def cons[B >: A](v: B): List[B]

Теперь это действительно. Вы можете себе представить , что Aв той или иной вниз, но Bможет изменяться вверх относительно , Aтак как Aэто его нижняя граница. С этим объявлением метода мы можем Aбыть ковариантными, и все работает.

Обратите внимание, что этот прием работает только в том случае, если мы возвращаем экземпляр List, специализирующийся на менее конкретном типе B. Если вы попытаетесь сделать Listизменчивым, все пойдет не так, как вы пытаетесь присвоить значения типа Bпеременной типа A, что запрещено компилятором. Всякий раз, когда у вас есть изменчивость, вам нужен какой-то мутатор, для которого требуется параметр метода определенного типа, который (вместе с аксессором) подразумевает инвариантность. Covariance работает с неизменяемыми данными, поскольку единственной возможной операцией является метод доступа, которому может быть задан ковариантный тип возврата.

Даниэль Спивак
источник
4
Можно ли это сформулировать на простом английском языке как - вы можете взять что-то более простое в качестве параметра и вы можете вернуть что-то более сложное?
Фил
1
Компилятор Java (1.7.0) не компилирует "Object [] arr = new int [1];" вместо этого выдается сообщение об ошибке: «java: требуются несовместимые типы: java.lang.Object [] found: int []». Я думаю, что вы имели в виду "Object [] arr = new Integer [1];".
Эмре Севинч
2
Когда вы упомянули: «Причина этого правила оставлена ​​читателю в качестве упражнения (подсказка: подумайте о разных случаях, когда функции имеют подтипы, как в примере с моим массивом выше)». Не могли бы вы привести пару примеров?
Perryzheng
2
@perryzheng на это , возьмите trait Animal, trait Cow extends Animal, def iNeedACowHerder(herder: Cow => Unit, c: Cow) = herder(c)и def iNeedAnAnimalHerder(herder: Animal => Unit, a: Animal) = herder(a). Тогда, iNeedACowHerder({ a: Animal => println("I can herd any animal, including cows") }, new Cow {})все в порядке, поскольку наш пастух может пасти коров, но iNeedAnAnimalHerder({ c: Cow => println("I can herd only cows, not any animal") }, new Animal {})дает ошибку компиляции, так как наш пастух не может пасти всех животных.
Ласф
Это связано и помогло мне с отклонениями: typelevel.org/blog/2016/02/04/variance-and-functors.html
Питер Шмитц,
27

@Daniel объяснил это очень хорошо. Но объясним это вкратце, если бы это было разрешено:

  class Slot[+T](var some: T) {
    def get: T = some   
  }

  val slot: Slot[Dog] = new Slot[Dog](new Dog)   
  val slot2: Slot[Animal] = slot  //because of co-variance 
  slot2.some = new Animal   //legal as some is a var
  slot.get ??

slot.getзатем выдаст ошибку во время выполнения, так как она была неудачной при преобразовании Animalв Dog(duh!).

В целом изменчивость не сочетается с ковариацией и противоречивостью. Вот почему все коллекции Java инвариантны.

Jatin
источник
7

См. Scala на примере , стр. 57+ для полного обсуждения этого.

Если я правильно понимаю ваш комментарий, вам нужно перечитать отрывок, начинающийся внизу страницы 56 (в основном, то, что я думаю, что вы просите, не безопасно для типов без проверок во время выполнения, чего не делает scala, так что тебе не повезло). Переводим их пример для использования вашей конструкции:

val x = new Slot[String]("test") // Make a slot
val y: Slot[Any] = x             // Ok, 'cause String is a subtype of Any
y.set(new Rational(1, 2))        // Works, but now x.get() will blow up 

Если вы чувствуете, что я не понимаю ваш вопрос (вполне вероятная возможность), попробуйте добавить дополнительные объяснения / контекст к описанию проблемы, и я попробую еще раз.

В ответ на ваше редактирование: Неизменные слоты - это совсем другая ситуация ... * smile * Я надеюсь, что приведенный выше пример помог.

MarkusQ
источник
Я прочитал это; к сожалению, я (до сих пор) не понимаю, как я могу сделать то, что я спрашиваю выше (то есть на самом деле написать параметризованный ковариант класса в T)
oxbow_lakes
Я удалил свою оценку, так как понял, что это немного грубо. В вопросе (ах) я должен был четко указать, что я читал биты из Scala на собственном примере; Я просто хотел, чтобы это объяснили «менее формально»
oxbow_lakes
@oxbow_lakes smile Боюсь, Scala By Example - менее формальное объяснение. В лучшем случае мы можем попытаться использовать конкретные примеры для работы, хотя это здесь ...
MarkusQ
Извините - я не хочу, чтобы мой слот был изменчивым. Я только что понял, что проблема в том, что я объявил var, а не val
oxbow_lakes
3

Вам необходимо применить нижнюю границу к параметру. Мне трудно вспомнить синтаксис, но я думаю, что это будет выглядеть примерно так:

class Slot[+T, V <: T](var some: V) {
  //blah
}

Сложно понять Scala-by-example, несколько конкретных примеров помогло бы.

Saem
источник