Kovarianz und Kontravarianz in Scala (Teil 1)

Das Thema "Kovarianz und Kontravarianz" wird zwar auch bei Wikipedia erklärt, ich versuche es hier mit eigenen Worten und Scala.

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 CatAnimal 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 AB, 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 CatAnimal. 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:

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
: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 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@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)
: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 Begriffe erst einmal grundlegend erklärt. Wer sich weiter informieren möchte, dem empfehle ich das Buch von Odersky et. al.