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

Исходя из этого вопроса , может кто-нибудь объяснить в 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) { 
}

Ответы [ 4 ]

296 голосов
/ 23 марта 2009

Обычно параметр типа ковариантный - это параметр, который может изменяться в зависимости от подтипа класса (альтернативно, варьироваться в зависимости от подтипа, отсюда и префикс "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' он является противоположным. На английском языке это можно прочитать следующим образом:

Функция A является подтипом другой функции B , если тип параметра A является супертипом типа параметра B , в то время как тип возврата A является подтипом типа возврата 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 работает с неизменяемыми данными, поскольку единственной возможной операцией является метод доступа, которому может быть задан ковариантный тип возврата.

27 голосов
/ 17 декабря 2013

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

  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 (хм!).

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

7 голосов
/ 19 марта 2009

См. 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 

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

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

3 голосов
/ 19 марта 2009

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...