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 Animal
s schreiben
kann.
def mkPair(a: Animal, b: Animal) = (a,b)
Hier lassen sich Cat
s 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
⊑B
undG[A]
⊑G[B]
, dann istT
inG[T]
kovariant (covariant). -
Wenn
A
⊑B
undG[B]
⊑G[A]
, dann istT
inG[T]
kontravariant (contravariant). -
Wenn beides nicht gilt, ist
T
inG[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@10cf09e8
Und 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.meow
f
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 Cat
s und einen für Animal
s anlegen.
scala> val sc = new Setter[Cat]
sc: Setter[Cat] = Setter@2b30b627
scala> val sa = new Setter[Animal]
sa: Setter[Animal] = Setter@6b063695
Hier 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.