Kovarianz und Kontravarianz in Scala
July 06, 2015
Den Code gibt es auch bei Gist.
Klassenhierarchien: die ⊑-Relation
Betrachten wir die folgende Klassenhierarchie:
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.
Hier lassen sich Cat
s und Dogs
mischen.
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
.
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.
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
:
Hier kann ich mir z. B. einen Katzen-Getter anlegen:
Und später kann ich mir die Katze geben lassen:
Wir können aber auch einen Animal
-Getter anlegen:
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.
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:
und schreiben die folgende Funktion
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:
Auch hier können wir uns jeweils einen für Cat
s und einen für Animal
s anlegen.
Hier passiert jetzt beim Ausprobieren der verschiedenen Kombinationen das Folgende:
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.