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

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

Решение

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

@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 (Дух!).

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

См. 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 * Я надеюсь, что приведенный выше пример помог.

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

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

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

Другие вопросы по тегам