Warum wird das Beispiel nicht kompiliert, auch bekannt als wie funktioniert (Co-, Contra- und In-) Varianz?

147

Kann jemand im Anschluss an diese Frage in Scala Folgendes erklären:

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

}

Ich verstehe den Unterschied zwischen +Tund Tin der Typdeklaration (sie wird kompiliert, wenn ich sie verwende T). Aber wie schreibt man dann tatsächlich eine Klasse, deren Typparameter kovariant ist, ohne das nicht parametrisierte Objekt zu erstellen ? Wie kann ich sicherstellen, dass Folgendes nur mit einer Instanz von erstellt werden kann T?

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

BEARBEITEN - jetzt auf Folgendes zurückzuführen:

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

Das ist alles gut, aber ich habe jetzt zwei Typparameter, von denen ich nur einen möchte. Ich werde die Frage folgendermaßen erneut stellen:

Wie kann ich eine unveränderliche Slot Klasse schreiben, die in ihrem Typ kovariant ist ?

EDIT 2 : Duh! Ich habe benutzt varund nicht val. Folgendes wollte ich:

class Slot[+T] (val some: T) { 
}
oxbow_lakes
quelle
6
Weil vares einstellbar ist, während vales nicht ist. Es ist der gleiche Grund, warum Scalas unveränderliche Sammlungen kovariant sind, die veränderlichen jedoch nicht.
oxbow_lakes
Dies könnte in diesem Zusammenhang interessant sein: scala-lang.org/old/node/129
user573215

Antworten:

302

Im Allgemeinen ist ein kovarianter Typparameter einer, der während der Subtypisierung der Klasse nach unten variieren darf (alternativ mit der Subtypisierung variieren, daher das Präfix "co-"). Konkreter:

trait List[+A]

List[Int]ist ein Subtyp von List[AnyVal]weil Intist ein Subtyp von AnyVal. Dies bedeutet, dass Sie eine Instanz angeben können, in der List[Int]ein Wert vom Typ List[AnyVal]erwartet wird. Dies ist wirklich eine sehr intuitive Art und Weise, wie Generika arbeiten, aber es stellt sich heraus, dass es nicht gesund ist (das Typensystem bricht), wenn es in Gegenwart veränderlicher Daten verwendet wird. Aus diesem Grund sind Generika in Java unveränderlich. Kurzes Beispiel für Unklarheiten bei der Verwendung von Java-Arrays (die fälschlicherweise kovariant sind):

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

Wir haben gerade Stringeinem Array vom Typ einen Wert vom Typ zugewiesen Integer[]. Aus Gründen, die offensichtlich sein sollten, sind dies schlechte Nachrichten. Das Java-Typsystem ermöglicht dies tatsächlich zur Kompilierungszeit. Die JVM wird ArrayStoreExceptionzur Laufzeit "hilfreich" einen werfen . Das Typensystem von Scala verhindert dieses Problem, da der Typparameter für die ArrayKlasse unveränderlich ist (Deklaration ist [A]eher als [+A]).

Beachten Sie, dass es eine andere Art von Varianz gibt, die als Kontravarianz bekannt ist . Dies ist sehr wichtig, da es erklärt, warum Kovarianz einige Probleme verursachen kann. Kontravarianz ist buchstäblich das Gegenteil von Kovarianz: Die Parameter variieren mit der Subtypisierung nach oben . Es ist teilweise viel seltener, weil es so kontraintuitiv ist, obwohl es eine sehr wichtige Anwendung hat: Funktionen.

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

Beachten Sie die Varianzanmerkung " - " für den PTypparameter. Diese Erklärung als Ganzes bedeutet, dass sie Function1kontravariant Pund kovariant ist R. Somit können wir die folgenden Axiome ableiten:

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

Beachten Sie, dass T1'dies ein Subtyp (oder derselbe Typ) von sein muss T1, während es für T2und das Gegenteil ist T2'. Auf Englisch kann dies wie folgt gelesen werden:

Eine Funktion A ist ein Subtyp einer anderen Funktion B, wenn der Parametertyp von A ein Supertyp des Parametertyps von B ist, während der Rückgabetyp von A ein Subtyp des Rückgabetyps von B ist .

Der Grund für diese Regel bleibt dem Leser als Übung überlassen (Hinweis: Denken Sie an verschiedene Fälle, wenn Funktionen subtypisiert sind, wie in meinem Array-Beispiel von oben).

Mit Ihrem neu gewonnenen Wissen über Co- und Kontravarianz sollten Sie erkennen können, warum das folgende Beispiel nicht kompiliert wird:

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

Das Problem ist, dass Aes kovariant ist, während die consFunktion erwartet, dass ihr Typparameter invariant ist . Somit Aändert sich die falsche Richtung. Interessanterweise könnten wir dieses Problem lösen, indem wir ListContravariant in setzen A, aber dann wäre der Rückgabetyp List[A]ungültig, da die consFunktion erwartet, dass sein Rückgabetyp kovariant ist .

Unsere einzigen beiden Möglichkeiten sind: a) AInvariante zu machen , die netten, intuitiven Subtypisierungseigenschaften der Kovarianz zu verlieren, oder b) der consMethode, die Aals Untergrenze definiert , einen lokalen Typparameter hinzuzufügen :

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

Dies ist jetzt gültig. Sie können sich vorstellen , dass Anach unten variiert, aber in Bder Lage , sich nach oben zu variieren bezüglich Ada Aist die Untergrenze. Mit dieser Methodendeklaration können wir Akovariant sein und alles funktioniert.

Beachten Sie, dass dieser Trick nur funktioniert, wenn wir eine Instanz zurückgeben, Listdie auf den weniger spezifischen Typ spezialisiert ist B. Wenn Sie versuchen, Listveränderlich zu machen , brechen die Dinge zusammen, da Sie am Ende versuchen B, einer Variablen vom Typ Werte vom Typ zuzuweisen A, die vom Compiler nicht zugelassen werden. Wenn Sie veränderlich sind, benötigen Sie einen Mutator, für den ein Methodenparameter eines bestimmten Typs erforderlich ist, der (zusammen mit dem Accessor) eine Invarianz impliziert. Die Kovarianz arbeitet mit unveränderlichen Daten, da die einzig mögliche Operation ein Accessor ist, dem ein kovarianter Rückgabetyp zugewiesen werden kann.

Daniel Spiewak
quelle
4
Könnte dies in einfachem Englisch ausgedrückt werden als - Sie können etwas Einfacheres als Parameter nehmen und etwas Komplexeres zurückgeben?
Phil
1
Der Java-Compiler (1.7.0) kompiliert nicht "Object [] arr = new int [1];" sondern gibt die Fehlermeldung: "Java: Inkompatible Typen erforderlich: java.lang.Object [] gefunden: int []". Ich denke, Sie meinten "Object [] arr = new Integer [1];".
Emre Sevinç
2
Als Sie erwähnten: "Der Grund für diese Regel wird dem Leser als Übung überlassen (Hinweis: Denken Sie an verschiedene Fälle, wenn Funktionen subtypisiert sind, wie in meinem Array-Beispiel von oben)." Könnten Sie tatsächlich ein paar Beispiele nennen?
Perryzheng
2
@perryzheng pro dies , nehmen trait Animal, trait Cow extends Animal, def iNeedACowHerder(herder: Cow => Unit, c: Cow) = herder(c)und def iNeedAnAnimalHerder(herder: Animal => Unit, a: Animal) = herder(a). Dann iNeedACowHerder({ a: Animal => println("I can herd any animal, including cows") }, new Cow {})ist es okay, da unser Tierhirte Kühe hüten kann, aber iNeedAnAnimalHerder({ c: Cow => println("I can herd only cows, not any animal") }, new Animal {})einen Kompilierungsfehler gibt, da unser Kuhhirte nicht alle Tiere hüten kann.
Lasf
Dies ist verwandt und hat mir bei der Abweichung geholfen: typelevel.org/blog/2016/02/04/variance-and-functors.html
Peter Schmitz
27

@ Daniel hat es sehr gut erklärt. Aber um es kurz zu erklären, wenn es erlaubt war:

  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.getwird dann zur Laufzeit einen Fehler auslösen, da die Konvertierung von Animalnach Dog(duh!) nicht erfolgreich war.

Im Allgemeinen passt die Mutabilität nicht gut zu Co-Varianz und Contra-Varianz. Aus diesem Grund sind alle Java-Sammlungen unveränderlich.

Jatin
quelle
7

Eine ausführliche Beschreibung finden Sie unter Scala anhand eines Beispiels , Seite 57+.

Wenn ich Ihren Kommentar richtig verstehe, müssen Sie die Passage ab Ende 56 erneut lesen (im Grunde genommen ist das, was Sie verlangen, ohne Laufzeitprüfungen nicht typsicher, was Scala nicht tut. du hast also kein Glück). Übersetzen Sie ihr Beispiel, um Ihr Konstrukt zu verwenden:

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 

Wenn Sie der Meinung sind, dass ich Ihre Frage nicht verstehe (eine eindeutige Möglichkeit), fügen Sie der Problembeschreibung weitere Erklärungen / Zusammenhänge hinzu, und ich werde es erneut versuchen.

Als Antwort auf Ihre Bearbeitung: Unveränderliche Slots sind eine ganz andere Situation ... * lächeln * Ich hoffe, das obige Beispiel hat geholfen.

MarkusQ
quelle
Ich habe das gelesen; Leider verstehe ich (immer noch) nicht, wie ich das tun kann, was ich oben verlange (dh tatsächlich eine parametrisierte Klassenkovariante in T schreiben)
oxbow_lakes
Ich entfernte mein Downmark, als mir klar wurde, dass dies etwas hart war. Ich hätte in den Fragen klarstellen sollen, dass ich die Teile von Scala anhand eines Beispiels gelesen hatte. Ich wollte nur, dass es auf "weniger formale" Weise erklärt wird
oxbow_lakes
@oxbow_lakes Lächeln Ich fürchte, Scala By Example ist die weniger formale Erklärung. Bestenfalls können wir versuchen, konkrete Beispiele zu verwenden, um hier zu arbeiten ...
MarkusQ
Entschuldigung - ich möchte nicht, dass mein Slot veränderlich ist. Ich habe gerade festgestellt, dass das Problem darin besteht, dass ich var und nicht val
oxbow_lakes deklariert habe.
3

Sie müssen eine Untergrenze für den Parameter anwenden. Es fällt mir schwer, mich an die Syntax zu erinnern, aber ich denke, sie würde ungefähr so ​​aussehen:

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

Die Scala-by-Example ist etwas schwer zu verstehen, ein paar konkrete Beispiele hätten geholfen.

Saem
quelle