Kovarianz und Kontravarianz in Scala
July 06, 2015
Den Code gibt es auch bei Gist.
Klassenhierarchien: die ⊑-Relation
Betrachten wir die folgende Klassenhierarchie:
abstract class Animal
class Cat extends Animal
class Dog extends Animal
Die Subtype-Relation Cat ⊑ Animal besagt, dass Cat ein Subtyp von
Animal ist. Man sagt umgangssprachlich auch, dass eine Cat ein Animal ist
und nennt dieses auch is-a-Hierarchie ("is a" für "ist ein/e").
Man darf das Symbol ⊑ aber nicht als "kleiner-als" übersetzen, obwohl es ähnlich aussieht. Denn eine Katze
ist zwar ein Subtyp, besitzt aber evtl. mehr Informationen und Methoden.
Ein Vorteil einer solchen Klassenhierarchie ist, dass man z. B. eine Funktion für alle Animals schreiben
kann.
def mkPair(a: Animal, b: Animal) = (a,b)
Hier lassen sich Cats und Dogs mischen.
scala> mkPair(new Cat, new Dog)
res12: (Animal, Animal) = (Cat@77f991c,Dog@3a7e365)
Allgemein gesprochen kann ich an jeder Stelle an der ein Animal erwartet wird auch einen Untertyp
Cat oder Dog verwenden.
In der Programmiersprachentheorie wird diese Eigenschaft
das Liskov'sche Substitutionsprinzip
genannt.
Wenn A ⊑ B, dann können Ausdrücke vom Typ B durch Ausdrücke vom Typ
A ersetzt / substituiert werden.
Generische Klassen mit Typparametern
Eine generische Klasse hat mindestens einen Typparameter T.
abstract class G[T] {
def doThis(val: T): T
def doThat(): T
}
Dieses ist z. B. bei Listen, Mengen und Bäumen und anderen
"Collections" nützlich. So braucht man nur einmal List[T] implementieren und kann dann Listen von Katzen,
von Hunden oder von gemischten Tieren erstellen.
scala> val cs = List(new Cat, new Cat)
cs: List[Cat] = List(Cat@4b03cbad, Cat@5b29ab61)
scala> val ds = List(new Dog, new Dog)
ds: List[Dog] = List(Dog@68e47e7, Dog@1c00d406)
scala> val as = List(new Cat, new Dog)
as: List[Animal] = List(Cat@6b030101, Dog@60a4e619)Generische Klassen und die ⊑-Relation: Kovarianz und Kontravarianz
Bei Typparametern von generischen Klassen taucht die Frage auf, wie sie sich zur ⊑-Relation verhalten.
Wir wissen, dass Cat ⊑ Animal. Wie ist es aber mit
G[Cat] und G[Animal]? Oder umgangssprachlich gefragt: wenn eine Cat ein
Animal ist, ist ein G[Cat] dann auch ein G[Animal]?
Hier gibt es die folgenden Möglichkeiten:
-
Wenn
A⊑BundG[A]⊑G[B], dann istTinG[T]kovariant (covariant). -
Wenn
A⊑BundG[B]⊑G[A], dann istTinG[T]kontravariant (contravariant). -
Wenn beides nicht gilt, ist
TinG[T]invariant.
Wir werden uns im Folgenden möglichst einfache Beispiele angucken. Bei einer Klasse gibt es nur zwei Richtungen, in die Informationen fließen können: hinein oder heraus bzw. schreiben oder lesen. Methoden, die Daten aus Klassen lesen, werden Getter genannt und Methoden die schreiben werden Setter genannt.
Getter
Betrachen wir jetzt eine möglichst minimalen Getter mit dem Typparameter T:
class Getter[T](val value: T) {
def get = value
}Hier kann ich mir z. B. einen Katzen-Getter anlegen:
scala> val gc = new Getter(new Cat)
gc: Getter[Cat] = Getter@10cf09e8Und später kann ich mir die Katze geben lassen:
scala> gc.get
res0: Cat = Cat@3a0baae5
Wir können aber auch einen Animal-Getter anlegen:
scala> val ga = new Getter[Animal](new Cat)
ga: Getter[Animal] = Getter@5f574cc2
Das ist der gleiche Code, nur das der Getter jetzt ein Getter[Animal] ist, der auch
eine Animal zurückgibt, obwohl wir im Konstruktor eine new Cat
hineingesteckt haben.
Wie ist jetzt das Verhältnis von Getter[Cat] und Getter[Animal]?
Können wir jedes Vorkommen von Getter[Cat] durch ein Getter[Animal] ersetzen
oder umgekehrt?
Rufen wir die Getter mal auf und versuchen, die Ergebnisse zu konvertieren.
scala> gc.get
res9: Cat = Cat@3dddbe65
scala> gc.get : Animal
res10: Animal = Cat@3dddbe65
scala> ga.get
res11: Animal = Cat@62f87c44
scala> ga.get : Cat
<console>:12: error: type mismatch;
found : Animal
required: Cat
ga.get : Cat
^
Das Ergebnis von gc.get lässt sich in ein Animal konvertieren,
während sich das Ergebnis
von ga.get nicht in eine Katze konvertieren lässt.
Also ist gc allgemeiner als ga. Wir können ga nicht überall verwenden
und gc nicht durch ga substituieren.
Damit ist Getter[Cat] ⊑ Getter[Animal].
Aber das lässt sich auch noch anders zeigen. Erweitern wir die Katze mal um eine Methode:
class Cat extends Animal {
def meow() : Unit = println("meow")
}und schreiben die folgende Funktion
def f(g: Getter[Cat]): Unit = g.get.meowf akzeptiert keinen Getter[Animal] da hier nicht sichergestellt ist, dass
die Methode meow auch existiert. Umgekehrt kann jedes Vorkommen von Getter[Animal]
durch einen Getter[Cat] ersetzt werden.
Es gilt also Getter[Cat] ⊑ Getter[Animal] und T in
Getter ist kovariant.
Setter
Bei einem Setter wird nur geschrieben. Siehe z. B. die folgende Klasse, die einfach nur ein Argument annimmt und es sofort vergisst:
class Setter[T] {
def set(v: T): Unit = { }
}
Auch hier können wir uns jeweils einen für Cats und einen für Animals anlegen.
scala> val sc = new Setter[Cat]
sc: Setter[Cat] = Setter@2b30b627
scala> val sa = new Setter[Animal]
sa: Setter[Animal] = Setter@6b063695Hier passiert jetzt beim Ausprobieren der verschiedenen Kombinationen das Folgende:
scala> sa.set(new Cat)
scala> sa.set(new Cat: Animal)
scala> sc.set(new Cat)
scala> sc.set(new Cat: Animal)
<console>:12: error: type mismatch;
found : Animal
required: Cat
sc.set(new Cat: Animal)
^
Der Setter[Animal] kann alle Tiere annehmen, während der
Setter[Cat] nur Katzen nimmt (Ist ja eigentlich auch klar und logisch :-)
Ich kann also also Setter[Cat] durch Setter[Animal] austauschen.
Es gilt (siehe Substitutionsprinzip): Setter[Animal] ⊑ Setter[Cat]
und T in Setter[T] ist kontravariant.
Fazit
Damit wären die beiden Begriffe erst einmal grundlegend erklärt. Wer sich weiter informieren möchte, dem empfehle ich das Buch von Odersky et. al.