Чому приклад не складається, він же як працює (спів-, проти-, і-) дисперсія?


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] }
}

EDIT - тепер це зводиться до наступного:

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

це все добре, але зараз у мене два параметри типу, де я хочу лише одного. Я повторно поставлю питання таким чином:

Як я можу написати незмінний Slot клас, коваріантний за своїм типом?

ЕДИТ 2 : Да! Я використовував varі ні val. Наступне - чого я хотів:

class Slot[+T] (val some: T) { 
}

6
Тому що varвстановлено, поки valце не так. Це та сама причина, по якій незмінні колекції Scala є коваріантними, але ті, що змінюються - ні.
oxbow_lakes

Це може бути цікаво в цьому контексті: scala-lang.org/old/node/129
користувач573215

Відповіді:


302

Загалом параметр коваріантного типу - це той, якому дозволено змінюватись у міру підтипу класу (як варіант, змінюється підтипом, отже, і префікс "co-"). Більш конкретно:

trait List[+A]

List[Int]є підтипом, List[AnyVal]тому що Intє підтипом AnyVal. Це означає, що ви можете надати примірник, List[Int]коли List[AnyVal]очікується значення типу . Це дійсно дуже інтуїтивно зрозумілий спосіб роботи дженериків, але виявляється, що він невідомий (порушує систему типу) при використанні в присутності змінних даних. Ось чому дженерики є інваріантними на Яві. Короткий приклад невмілості використання масивів 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, яка заборонена компілятором. Кожного разу, коли у вас є мутабельність, вам потрібно мати якийсь мутатор, для якого потрібен параметр методу певного типу, який (разом з аксесуаром) передбачає інваріантність. Коваріація працює з незмінними даними, оскільки єдино можливою операцією є аксесуар, якому може бути наданий тип коваріантного повернення.


4
Чи можна це сказати простою англійською мовою як - ви можете взяти щось більш просте в якості параметра і ви можете повернути щось складніше?
Філ

1
Компілятор Java (1.7.0) не компілює "Object [] arr = new int [1];" але швидше подає повідомлення про помилку: "java: потрібні несумісні типи: java.lang.Object [] знайдено: 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 {})дає помилку при складанні, оскільки наш корівник не може погоняти всіх тварин.
Ласф

Це пов’язано і допомогло мені з варіацією
Пітер Шмітц

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 інваріантні.


7

Дивіться Scala на прикладі , стор. 57+ для повного обговорення цього питання.

Якщо я правильно розумію ваш коментар, вам потрібно перечитати уривок, починаючи внизу сторінки 56 (в основному, те, що я думаю, що ви просите, не є безпечним для типу без перевірки часу запуску, що скала не робить, значить, вам не пощастило). Перекладаючи їх приклад на використання вашої конструкції:

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 

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

У відповідь на вашу редакцію: Незмінні слоти - це зовсім інша ситуація ... * посмішка * Я сподіваюся, що наведений вище приклад допоміг.


Я це прочитав; на жаль, я (досі) не розумію, як я можу виконувати те, що я прошу вище (тобто фактично написати параметризований коваріант класу в T)
oxbow_lakes

Я зняв свій нижній показник, коли зрозумів, що це трохи суворо. У питанні (ях) я мав би зрозуміти, що я прочитав шматочки від Scala на прикладі; Я просто хотів, щоб це було пояснено "менш формально"
oxbow_lakes

@oxbow_lakes smile Я боюся, що Скала на прикладі є менш формальним поясненням. У кращому випадку ми можемо спробувати використовувати конкретні приклади для роботи, хоча це тут ...
MarkusQ

Вибачте - я не хочу, щоб мій слот був змінений. Я щойно зрозумів, що проблема полягає в тому, що я оголосив var, а не val
oxbow_lakes

3

Потрібно застосувати нижню межу параметра. Мені важко запам'ятати синтаксис, але я думаю, що це виглядатиме приблизно так:

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

Скала на прикладі трохи важко зрозуміти, кілька конкретних прикладів допомогли б.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.