Dlaczego przykład się nie kompiluje, czyli jak działa (współ-, przeciw- i nie-) wariancja?

147

Kontynuując to pytanie , czy ktoś może wyjaśnić w Scali, co następuje:

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

}

Rozumiem różnicę między +Tiw Tdeklaracji typu (kompiluje się, jeśli używam T). Ale jak właściwie napisać klasę, która jest kowariantna w swoim parametrze typu, bez uciekania się do tworzenia rzeczy nieparametryzowanej ? Jak mogę się upewnić, że następujące elementy można utworzyć tylko za pomocą wystąpienia T?

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

EDYCJA - teraz sprowadziliśmy się do następujących:

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

to wszystko jest w porządku, ale mam teraz dwa parametry typu, gdzie chcę tylko jeden. Ponownie zadam pytanie w ten sposób:

Jak napisać niezmienną Slot klasę, która jest kowariantna w swoim typie?

EDYCJA 2 : Duh! Użyłem vari nie val. Oto, czego chciałem:

class Slot[+T] (val some: T) { 
}
oxbow_lakes
źródło
6
Ponieważ można varto ustawić, podczas gdy valnie. Z tego samego powodu niezmienne kolekcje scali są kowariantne, a zmienne - nie.
oxbow_lakes
To może być interesujące w tym kontekście: scala-lang.org/old/node/129
user573215

Odpowiedzi:

302

Ogólnie rzecz biorąc, kowariantny parametr typu to taki, który może zmieniać się w dół, gdy klasa jest poddawana podtypowi (alternatywnie, zmieniać się wraz z podtypem, stąd prefiks „co-”). Bardziej konkretnie:

trait List[+A]

List[Int]jest podtypem List[AnyVal]ponieważ Intjest podtypem AnyVal. Oznacza to, że możesz podać wystąpienie, List[Int]gdy List[AnyVal]oczekiwana jest wartość typu . Jest to naprawdę bardzo intuicyjny sposób działania typów generycznych, ale okazuje się, że jest on nieuzasadniony (łamie system typów), gdy jest używany w obecności zmiennych danych. Dlatego typy generyczne są niezmienne w Javie. Krótki przykład nieprawidłowości przy użyciu tablic Java (które są błędnie kowariantne):

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

Właśnie przypisaliśmy wartość typu Stringdo tablicy typu Integer[]. Z powodów, które powinny być oczywiste, to zła wiadomość. System typów Javy faktycznie na to pozwala w czasie kompilacji. JVM „pomocnie” wyśle ​​plik ArrayStoreExceptionw czasie wykonywania. System typów Scali zapobiega temu problemowi, ponieważ parametr typu w Arrayklasie jest niezmienny (deklaracja jest [A]raczej niż [+A]).

Zauważ, że istnieje inny rodzaj wariancji zwany kontrawariancją . Jest to bardzo ważne, ponieważ wyjaśnia, dlaczego kowariancja może powodować pewne problemy. Kontrawariancja jest dosłownie przeciwieństwem kowariancji: parametry zmieniają się w górę wraz z podtypami. Jest o wiele mniej powszechny, częściowo dlatego, że jest tak sprzeczny z intuicją, chociaż ma jedną bardzo ważną aplikację: funkcje.

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

Zwróć uwagę na adnotację wariancji „ - ” w Pparametrze typu. Ta deklaracja jako całość oznacza, że Function1jest sprzeczna Pi kowariantna w R. W ten sposób możemy wyprowadzić następujące aksjomaty:

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

Zauważ, że T1'musi to być podtyp (lub tego samego typu) T1, podczas gdy jest odwrotnie w przypadku T2i T2'. W języku angielskim można to odczytać w następujący sposób:

Funkcja jest podtypem innej funkcji B , jeśli typ parametr A jest supertypem typu parametru B, podczas gdy typ powrót A jest podtypem typu powrotnej B .

Czytelnikowi pozostawiono powód tej reguły jako ćwiczenie (wskazówka: pomyśl o różnych przypadkach, ponieważ funkcje są podtytułowane, jak mój przykład tablicy z góry).

Mając nowo odkrytą wiedzę na temat współ- i kontrawariancji, powinieneś być w stanie zrozumieć, dlaczego następujący przykład nie zostanie skompilowany:

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

Problem w tym, że Ajest kowariantny, podczas gdy consfunkcja oczekuje, że jej parametr typu będzie niezmienny . W ten sposób Azmienia zły kierunek. Co ciekawe, moglibyśmy rozwiązać ten problem, wprowadzając Listkontrawariantność w A, ale wtedy typ zwracany List[A]byłby nieprawidłowy, ponieważ consfunkcja oczekuje, że jego typ zwracany będzie kowariantny .

Nasze jedyne dwie opcje to a) uczynić Aniezmienność, tracąc ładne, intuicyjne właściwości kowariancji podtypów, lub b) dodać lokalny parametr typu do consmetody, która definiuje Ajako dolną granicę:

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

To jest teraz ważne. Możesz sobie wyobrazić, że Azmienia się w dół, ale Bmoże się zmieniać w górę w odniesieniu do, Aponieważ Ajest to jego dolna granica. Dzięki tej deklaracji metody możemy Abyć kowariantnymi i wszystko się ułoży.

Zauważ, że ta sztuczka działa tylko wtedy, gdy zwrócimy instancję, Listktóra jest wyspecjalizowana w mniej konkretnym typie B. Jeśli spróbujesz uczynić Listmutable, wszystko się psuje, ponieważ w końcu próbujesz przypisać wartości typu Bdo zmiennej typu A, co jest zabronione przez kompilator. Ilekroć masz zmienność, musisz mieć pewnego rodzaju mutator, który wymaga parametru metody określonego typu, co (wraz z akcesorium) implikuje niezmienność. Kowariancja działa z niezmiennymi danymi, ponieważ jedyną możliwą operacją jest akcesor, któremu można nadać kowariantny typ zwrotu.

Daniel Śpiewak
źródło
4
Czy można to wyrazić prostym językiem, jako - możesz wziąć coś prostszego jako parametr i możesz zwrócić coś bardziej złożonego?
Phil
1
Kompilator Java (1.7.0) nie kompiluje „Object [] arr = new int [1];” ale raczej wyświetla komunikat o błędzie: „java: wymagane niezgodne typy: znaleziono java.lang.Object []: int []”. Myślę, że miałeś na myśli „Object [] arr = new Integer [1];”.
Emre Sevinç
2
Kiedy wspomniałeś: „Powód dla tej reguły jest pozostawiony czytelnikowi jako ćwiczenie (wskazówka: pomyśl o różnych przypadkach, ponieważ funkcje są podtytułowane, jak mój przykład tablicy z góry)”. Czy mógłbyś podać kilka przykładów?
perryzheng,
2
@perryzheng za to , wziąć trait Animal, trait Cow extends Animal, def iNeedACowHerder(herder: Cow => Unit, c: Cow) = herder(c)i def iNeedAnAnimalHerder(herder: Animal => Unit, a: Animal) = herder(a). W takim razie iNeedACowHerder({ a: Animal => println("I can herd any animal, including cows") }, new Cow {})jest w porządku, ponieważ nasz pasterz zwierząt może stawiać krowy, ale iNeedAnAnimalHerder({ c: Cow => println("I can herd only cows, not any animal") }, new Animal {})popełnia błąd kompilacji, ponieważ nasz pasterz nie może stawiać wszystkich zwierząt.
Lasf
Jest to powiązane i pomogło mi w wariancji: typelevel.org/blog/2016/02/04/variance-and-functors.html
Peter Schmitz
27

@Daniel wyjaśnił to bardzo dobrze. Ale żeby wyjaśnić to w skrócie, gdyby było to dozwolone:

  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.getzgłosi wtedy błąd w czasie wykonywania, ponieważ nie powiodła się konwersja an Animalna Dog(duh!).

Ogólnie rzecz biorąc, zmienność nie pasuje dobrze do współwariancji i przeciwwariancji. To jest powód, dla którego wszystkie kolekcje Java są niezmienne.

Jatin
źródło
7

Zobacz przykład Scali , strona 57+, aby uzyskać pełne omówienie tego.

Jeśli dobrze rozumiem Twój komentarz, musisz ponownie przeczytać fragment zaczynający się na dole strony 56 (w zasadzie to, o co myślę, że prosisz, nie jest bezpieczne dla typów bez sprawdzania czasu wykonywania, czego Scala nie robi, więc nie masz szczęścia). Tłumaczenie ich przykładu, aby użyć twojej konstrukcji:

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 

Jeśli uważasz, że nie rozumiem twojego pytania (wyraźna możliwość), spróbuj dodać więcej wyjaśnienia / kontekstu do opisu problemu, a spróbuję ponownie.

W odpowiedzi na twoją edycję: Niezmienne automaty to zupełnie inna sytuacja ... * uśmiech * Mam nadzieję, że powyższy przykład pomógł.

MarkusQ
źródło
Przeczytałem to; niestety (nadal) nie rozumiem, jak mogę zrobić to, o co pytam powyżej (czyli właściwie napisać kowariantę sparametryzowanej klasy w T)
oxbow_lakes
Usunąłem swój downmark, ponieważ zdałem sobie sprawę, że to było trochę trudne. Powinienem był wyjaśnić w pytaniu (-ach), że przeczytałem fragmenty ze Scali na przykładzie; Chciałem tylko wyjaśnić to w "mniej formalny" sposób
oxbow_lakes
@oxbow_lakes smile Obawiam się, że Scala na przykładzie jest mniej formalnym wyjaśnieniem. W najlepszym przypadku możemy spróbować użyć konkretnych przykładów, aby to przepracować tutaj ...
MarkusQ
Przepraszam - nie chcę, aby mój automat był zmienny. Właśnie zdałem sobie sprawę, że problem polega na tym, że zadeklarowałem var, a nie val
oxbow_lakes
3

Musisz zastosować dolną granicę parametru. Trudno mi zapamiętać składnię, ale myślę, że wyglądałoby to mniej więcej tak:

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

Przykład Scali jest nieco trudny do zrozumienia, kilka konkretnych przykładów mogłoby pomóc.

Saem
źródło