Artikel

 
Dezember 2010 | Artikel

Advanced Scala – Varianz

(Link zum Artikel: http://www.it-republik.de/jaxenter/artikel/3475)

Teil 6 - Oder: “Ist eine Kiste voller Äpfel auch eine Kiste voller Obst?”

Text: Heiko Seeberger
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Ist eine Kiste voller Äpfel auch eine Kiste voller Obst? Der gesunde Menschenverstand würde vermutlich mit “Aber sicher!” antworten, schließlich zählen Äpfel zum Obst. Aber in der Objektorientierung bzw. beim Vererbungs-Polymorphismus müssen wir ein bisschen vorsichtiger sein. Warum?
Teil 1   Teil 2   

Nun, solange wir nur “Dinge” aus der Apfel-Kiste heraus nehmen, ist die Welt noch in Ordnung, selbst wenn wir sie als Obst-Kiste betrachten. Denn wenn die Obst-Kiste in Wahrheit eine Apfel-Kiste ist, dann kommen natürlich nur Äpfel heraus, die wir getrost als Obst betrachten können.

Aber nun drehen wir den Spieß einmal um und packen “Dinge” zur Kiste hinzu. Wenn wir nun wieder unsere Apfel-Kiste als Obst-Kiste betrachten, dann dürften wir beliebiges Obst hineingeben, also auch Bananen, Pfirsiche etc. Und dann wäre die Apfel-Kiste ja keine Apfel-Kiste mehr. Wer denkt, dass das nicht so schlimm wäre, der denke bitte an einen Transportkäfig für Hunde - und wie dort ein Elefant hineinpassen soll.

Etwas formaler dreht sich diese Problematik um die Frage nach der sogenannten Varianz: Besteht aufgrund einer Vererbungsbeziehung zwischen Objekten ebenfalls eine Vererbungsbeziehung zwischen generischen Objekten, die mit den ersten parametrisiert sind? Falls nicht, so nennt man das invariant. Falls schon, so gilt es noch, die Richtungen der Vererbungsbeziehungen zu betrachten. Wenn diese gleichgerichtet sind, nennt man das kovariant, andernfalls kontravariant. Letzteres mag zunächst komisch anmuten, es gibt aber durchaus plausible Beispiele, wie wir später sehen werden.

Obiges Obst-Beispiel könnten wir in Scala folgendermaßen implementieren:

  1. class Fruit
  2. class Apple extends Fruit
  3. class Box[A <: Fruit]

Wie verhält es sich hier mit der Varianz? Probieren wir es einfach aus:

  1. scala> val fruitBox: Box[Fruit] = new Box[Apple]
  2. <console>:8: error: type mismatch;
  3. ...
  4. scala> val appleBox: Box[Apple] = new Box[Fruit]
  5. <console>:8: error: type mismatch;
  6. ...

Das Beispiel zeigt, dass generische (parametrisierte) Typen in Scala grundsätzlich invariant sind. Wie wir anhand der Obst-Kiste gesehen haben, macht das im Allgemeinen durchaus Sinn.

Kovarianz
Aber wie wir ebenfalls gesehen haben, können wir dem Obst-Beispiel auch einen Spezialfall abgewinnen: Wenn wir uns dazu verpflichten, dass wir nichts mehr in die Kiste hinein geben, sondern nur heraus nehmen, dann wird unsere Kiste kovariant. In Scala können wir diese Verpflichtung zur Kovarianz durch Voranstellen eines Plus-Zeichens “+” vor den Typparameter deklarieren:

  1. class Box[+A <: Fruit]

Und siehe da, auf einmal ist auch für den Compiler eine Kiste mit Äpfeln eine Obst-Kiste:

  1. scala> val fruitBox: Box[Fruit] = new Box[Apple]
  2. fruitBox: Box[Fruit] = Box@6a754384

Aber was bedeutet das für unser Problem? Welche Auswirkung hat das Plus-Zeichen auf den Scala-Code? Wie wird sichergestellt, dass wir nichts mehr in die Kiste hinein geben können? Um das zu verstehen, müssen wir unser Beispiel ein bisschen erweitern. Zunächst fügen wir zur Box ein unveränderliches Feld hinzu:

  1. class Box[+A <: Fruit](val fruit: A)

Nun können wir eine Kiste mit einem Apfel erzeugen und einem Wert vom Typ Obst-Kiste zuweisen:

  1. scala> val fruitBox: Box[Fruit] = new Box(new Apple)
  2. fruitBox: Box[Fruit] = Box@3c7a279c
  3. scala> fruitBox.fruit
  4. res7: Fruit = Apple@154174f9

Wir können natürlich von der Obst-Kiste den Inhalt abfragen. Aber wichtig ist, dass wir diesen nicht mehr verändern können. Damit das überhaupt möglich wäre, bräuchten wir eine Methode, die einen Parameter vom Typ A hat. Oder einfach ein veränderliches Feld statt eines unveränderlichen:

  1. scala> class Box[+A <: Fruit](var fruit: A)
  2. <console>:6: error: covariant type A occurs in contravariant position in type A of parameter of setter fruit_=
  3. ...

Wie wir sehen, lässt der Compiler so etwas aber gar nicht erst zu: Wenn wir einen generischen Typen als kovariant in einem seiner Typparameter deklarieren, dann darf dieser Typ nicht mehr an sogenannten kontravarianten Positionen auftauchen. Letztere sind schlicht Parameter von Methoden oder schreibbare Felder.

Teil 1   Teil 2   

andere Artikel dieser Serie


Anzeige

Kommentare

Gravatar Steve 06.12.2010
um 10:53 Uhr
Danke, eine sehr anschauliche Beschreibung!

Eine Korrektur auf der 2. Seite, Fazit: "der hat vollommen recht" -> "der hat vollkommen recht"
#zitieren
Gravatar Redaktion JAXenter 06.12.2010
um 11:15 Uhr
Der Typo wurde korrigiert.

Vielen Dank für den Hinweis!
#zitieren
Gravatar Heiko Seeberger 07.12.2010
um 15:21 Uhr
Ein gutes Beispiel für Kontravarianz, auf das mich mein Kollege Roman Roelofsen gebracht hat, sind Parameter von Funktionen.

Als Beispiel der Trait für Funktionen mit einem Parameter:
trait Function1 [-T1, +R]

Ganz allgemein sind immer alle Parameter kontravariant und das Resultat kovariant.

Das bedeutet u.a., dass der folgende Code korrekt ist:

scala> val stringIntFunc: Any => Int = a => a.toString.length
stringIntFunc: (Any) => Int =

scala> val intIntFunc: Int => Int = stringIntFunc
intIntFunction: (Int) => Int =
#zitieren
Gravatar Trepper 07.12.2010
um 16:31 Uhr
Wenn man statische Typisierung zu Ende denkt, kommt man bei einem komplizierten Typsystem wie dem von Scala raus. Aber ich stelle mir die Frage: lohnt sich der Aufwand wirklich? Wer will Methodensignaturen wie "def :+ [B >: A, That] (elem: B)(implicit bf: CanBuildFrom[List[A], B, That]) : That " lesen und verstehen?

Aller Vorteile statischer Typisierung zum Trotz, glaube ich, dass man z. B. mit Python schneller zum Ziel kommt und Schnelligkeit ist immer wichtiger in einem Markt, wo oft nur der erste wirklich groß wird.
#zitieren
Gravatar steve 07.12.2010
um 18:39 Uhr
Ich denke, da liegst du falsch.

Wir leben heute in einer Zeit, in der man versucht möglichst viele Softwarekomponenten wiederzuverwenden. Da ist dieser PHP-Python-Wischiwaschi-Ansatz einfach unbrauchbar.

Klar, im Vergleich zu Java hat man relativ schnell etwas hingeklimpert, aber sobald man etwas warten oder anpassen muss, kommen die langen Gesichter.

Ich denke dynamische Sprachen wie Python oder PHP bieten gegenüber Scala grundsätzlich keine Vorteile.

Man hat mit Scala weniger Schreibarbeit, bei gleichzeitiger besserer Hilfe des Compilers, der einem schon zur Compilezeit auf Fehler hinweist. Zudem ist die Syntax erheblicher gleichmäßiger als in Python, in der man je nach belieben Methodenaufrufe, englische Worte, spezielle Operatoren durcheinander wirft.

Natürlich kann man sich gegenüber besseren Typsystemen verweigern, aber deren fortschreitende Entwicklung lässt sich nicht aufhalten.

Ein Blick in die Vergangenheit zeigt, dass wir uns auf der einen Seite langsam von den speicher-unsicheren Sprachen über speicher-sichere, abstraktions-sichere, kapabilitäts-sichere Sprachen hin zu "puren" und totalen Sprachen entwickeln.

Orthogonal dazu stieg im Laufe der Zeit auch die Mächtigkeit der Typsysteme an: Von den untypisierten (PHP, Python, Ruby, Assembler, Forth) über first-order (C, Objective-C), Hindley-Milner (ML, SML, Haskell '98), second order (Java, C#, D), higher-kinded (Haskell (GHC), Scala) hin zu Dependent Typesystemen (Agda, Epigram).

Im Gegensatz zu untypisierten Sprachen passiert da jede Woche sowohl interessante Grundlagenforschung wie auch praktische Weiterentwicklung.

Bei den untypisierten Sprachen war bei LISP Schluss:
Alles danach ist nur LISP mit anderer Syntax, weniger mächtigeren Funktionen und mehr Bugs.

Als "Beweise" für die "Komplexität" des Typsystems ständig eine komplizierte Typ-Signatur herauszusuchen, wenn die zur Anzeige gedachte Typsignatur daneben steht ("def +: (elem: A) : List[A]") ist reichlich langweilig.

Zeige doch lieber, wie du die gleiche Funktionalität in einer anderen typsicheren Sprache besser und einfacher implementiert hättest. Dann kann man darüber diskutieren und auch sicherlich noch etwas lernen.
#zitieren
Gravatar Trepper 07.12.2010
um 19:22 Uhr
@Steve Warum der gereizte Unterton? Fühlst du dich angegriffen, weil ich kritisch über deine Lieblingssprache nachgedacht habe?

Es mag sein, dass die akademische Forschung in Richtung immer anspruchsvollerer Typsysteme geht, aber die Frage ist, ob die Entwickler mitgehen. PHP ist zwar eine fürchterliche Sprache, erfreut sich aber hartnäckig großer Beliebtheit. Viele Java-Entwickler haben sogar der statischen Typisierung den Rücken gekehrt und sind mit Ruby on Rails glücklich.
#zitieren
Gravatar Joe 07.12.2010
um 21:54 Uhr
Für die Akten: Python ist stark typisiert (allerdings halt dynamisch) #zitieren
Gravatar Gerolf Seitz 08.12.2010
um 00:58 Uhr
Korrektur für Seite 2:
"Ebenso liegt auf der Hand, dass der Compiler wohl kaum obige für Kovarianz funktionierende Kiste als kovariant durchgehen lässt:"
"kovariant" sollte wohl eher "kontravariant" sein.
#zitieren
Gravatar steve 08.12.2010
um 17:21 Uhr
@Trepper: Oh, tut mir Leid, wenn ich gereizt geklungen habe.

Ich war es nicht und wollte auch nicht so herüberkommen.
#zitieren

Anzeige

zurück zum Seitenanfang