Friday, October 29, 2010

Strukturelle Typsysteme - Ducktyping Light in statischen Sprachen

In meinem letzten Beitrag habe ich grob die Typen und Kategorien aufgelistet, die für Typsysteme eine Rolle spielen. Da ich die Kategorien alle nur sehr kurz angeschnitten habe, will ich einige der unbekannteren Aspekte noch einmal detaillierter beschreiben.

Von allen Kategorien / Bereichen zur Einteilung von Typsystemen sind die strukturellen Typsysteme neben dem Multi-Dispatch wahrscheinlich am wenigsten bekannt. Das ist eigentlich sehr schade, denn sie bilden das fehlende Glied zwischen den aus der Objektorientierung bekannten nominellen und den dynamischen Typsystemen. Doch halt.. Fangen wir von vorne an:

Die meisten Sprachen mit statischen Typsysteme verwenden ein nominatives Typsystem (auch als nominelles  Typsystem bezeichnet). In nominativen Typsystemen müssen die Typen an allem, was nach außen sichtbar ist (=> Methoden) explizit annotiert werden und schränken hier die möglichen Wertemengen auf den benannten (=> nominellen) Typ ein. Das härteste Beispiel hierfür ist C. Einen etwas flexibleren Weg gehen nominative Typsysteme, die die Subtypisierung erlauben. In der Praxis werden diese Typsysteme auch als "Objektorientiert" bezeichnet. In diesen Typsystemen ist es möglich, nicht nur den Typen selbst an eine Methode zu übergeben, sondern es sind auch Subtypen dieses Typs erlaubt. Soweit wahrscheinlich nichts neues...

Was aber sind strukturelle Typsysteme? Strukturelle Typsysteme sind relaxter als nominelle Typsysteme bei der Gültigkeit. Sie definieren den Typ nicht anhand des Namens, sondern anhand seiner Schnittstelle. Die Schnittstelle kennen auch nominelle Typsysteme: Sie besagt überlichweise, welche Operationen auf einem Typ möglich sind, z.B. isEmpty(), canJump() etc.
Der große Unterschied ist nun aber, das bei strukturellen Typsystemen die Beschränkung der Subklassenbildung wegfällt: Ein Wert entspricht dann einem bestimmten Typ, wenn er alle Methoden der Schnittstelle implementiert, unabhängig davon, ob er vom diesem Typ ableitet.

Beispiel (Scala)

type Duck = {
  def quack()
  def swim(speed: Int)
}

class Swan {
  def quack {
    println("swan")
  }
  def swim(speed: Int){
    println("Is swimming")
  }
}

object Main{
  def main(s: Array[String]){
    val duck: Duck = new Swan()
    duck.swim()
  }
}

Das Ergebnis: Das Programm wird vom Compiler akzeptiert, obwohl ein Swan nicht von Duck erbt!
Das ist ja auch vollkommen korrekt, weil der Compiler leicht verifizieren kann, das Swan alle Requirements der Schnittstelle Duck erfüllt und sich damit ordungsgemäß als "Duck" verwenden lassen kann.
Wie der Name schon andeutet: Ja, es handelt sich hierbei um ein Konstrukt das dem sehr ähnlich ist, das dynamische Sprachen auch als Duck-Typing bezeichnen. Strukturelle Typsysteme ermöglichen es, dieses dynamische Konstrukt in einem statischen Rahmen auszudrücken.

Strukturelle Typsysteme sind dadurch deutlich mächtiger als nominelle Typsysteme. Insbesondere entfällt das Problem, dass man in einem Fremdsystem normalerweise keine vorhandenen Typen mehr anpassen kann. So ist es z.B. nicht möglich, java.util.List auch in einem anderen Kontext zu benutzen, in dem man eine Schnittstelle hat, die z.B. nur die Methode "isEmpty" verlangt, obwohl der Compilr dieses Konstrukt als sicher identifizieren kann. Warum? Weil man der Klasse java.util.List keine neuen Schnittstellen mehr hinzufügen kann!

Außerdem gibt es noch einen weiteren Vorteil, den man am besten in Haskell nachvollziehen kann: In einem strukturellen Typsystem ist es theoretisch möglich, ein komplettes Programm typsicher zu schreiben, ohne ein einziges Mal eine Typannotation zu verwenden. Der Compiler ist (fast) immer in der Lage, einen entsprechenden strukturellen Typ ad-hoc festzustellen.

Beispiel (fiktives Scala)
def walk(person, steplength, distance){
  val steps = 0
  while (steps < distance){
    person.step(steplength)
    steps += steplength
  }  
}

In diesem kleinen Programm kann der Compiler die beiden Typen steplength, und distance relativ leicht anhand des typs step als int identifizieren. Was aber mit Person? Für Person reicht es, wenn er intern einen neuen strukturellen Typ definiert, der wie folgt aufgebaut ist:

type = {
  def step(steplength: Int)
}

Es ist also möglich, Typen implizit durch Typinferenz zu ermitteln und zur Validierung von Methoden zu verwenden! Auf diese Weise kommen strukturelle Typsysteme der Ausdruckskraft der dynamischen Sprachen sehr nahe, haben jedoch nicht deren Unsicherheit! Es können im obigen Beispiel nämlich wirklich nur Werte übergeben werden, die auch die Methodensignatur von "step" aufweisen. Dynamische Sprachen werden sich hingegen erst zur Laufzeit mit einer Exception "beschweren".

Anmerkung: Die obige Typinferenz an Methoden in Scala ist derzeit übrigens nur fiktiv: Scala verwendet zwar ein strukturelles Typsystem, erzwingt aber stets die Angabe der Typen an Methodensignaturen. Primär geschieht dies wahrscheinlich aus Performancegründen, da strukturelle Typen auf der JVM nur per Reflection umgesetzt werden können.

Ein häufiger Vorwurf gegenüber struktrullen Typsystemen ist, dass sie unsicherer sind als nominelle Typsysteme, da strukturelle Typen zu "leicht" vertauscht werden können. Diesen Vorwurf halte ich aber nicht für sehr valide, da deratige semantische Typfehler nur sehr selten vorkommen. Und selbst wenn: Nominelle Typsysteme können als eine Unterkategorie der strukturellen Typsysteme verwendet werden. So ist es möglich, in einem grundsätzlich flexiblen strukturellen Typsystem auch die mit härteren und "sicheren" Anforderungen versehenen nonimellen Typsysteme einzusetzen.

Das populärste Beispiel hierfür ist Scala. Eigentlich verwendet die Sprache ein strukturelles Typsystem. In der Praxis wird man aber immer "objektorientiert" mit nominativen Typen, den Klassen der Objektorientierung, arbeiten. Strukturelle Typen wird man nur dann einsetzen, wenn dies aufgrund fehlender Vererbungsbeziehungen nötig wird.

Links
http://en.wikipedia.org/wiki/Structural_type_system

No comments:

Post a Comment