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

Следуя этому вопросу, может кто-нибудь объяснить следующее в 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) { 
}

Ответы

Ответ 1

В общем случае параметр ковариантного типа - это тот, который разрешается изменять, поскольку класс подтипирован (в качестве альтернативы, изменяется с подтипированием, следовательно, префикс "ко-префикс" ). Более конкретно:

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 предотвращает эту проблему, поскольку параметр type в классе 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) сделать инвариант A, потерять приятные, интуитивные свойства типизации ковариации, или b) добавить параметр локального типа в метод cons, который определяет A как нижняя граница:

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

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

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

Ответ 2

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

  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 являются инвариантными.

Ответ 3

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

Ответ 4

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

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

Scala -образный пример немного трудно понять, несколько конкретных примеров помогли бы.