Sunday, October 31, 2010

Polymorphie - Die Vielformigkeit der Vielformigkeit


Polymorphie("Viele Formen") bedeutet in der Programmierung, dass ein Bezeichner abhängig vom Kontext unterschiedliche Bedeutungen hat. In Typsystemen ist Polymorphie ein sehr wichtiges Konzept um abstrakte Datentypen zu definieren (z.B. die Klassen der Objektorientierung). Nur extrem wenige Sprachen verwenden keinerlei dynamische Polymorphie - und diejenigen, die auf diese Kerneigenschaft verzichten, sind normalerweise.. scheiße. Beispiel: C hat war keine einzige Möglichkeit, durch dynamische Polymorphie Abstraktionen auszudrücken.

Der Polymorphismus hat ironischerweise selbst auch "viele Formen" und lässt sich in verschiedene Unterkategorien aufteilen. Interessanterweise können sich diese Unterkategorien zum Teil auch ordentlich in die Quere kommen. So sind z.B. das statische Method-Overloading und Vererbung zum Teil inkompatibel. Ich werde im folgenden nicht weiter darauf eingehen, aber vllt. dann mal wenn ich Kovarianz und Kontravarianz im Zusammenhang mit Typparametern (a.k.a. Generics) erkläre.

1. Ad-Hoc-Polymorphimus (Polymorphismus zur Compilezeit)
Polymorphimus zur Compilezeit bedeutet, das ad-hoc im Code Polymorphismus eingesetzt wird, um Bezeichner mit Bedeutungen zu überladen, dies aber nicht dynamisch geschieht, sondern durch den Compiler geprüft werden kann.
  • Method Overloading: Eine der bekanntesten Möglichkeiten ist sicherlich die Methodenüberladung. Abhängig von der Parameterliste wird je nach Aufruf eine andere Methode aufgerufen. Dies geschieht aber nicht zur Laufzeit sondern anhand der zur Compilezeit bekannten Typen. Besonders populär in Java, C++, C#.
  • Operator Overloading: Eigentlich nur ein Sonderfall des Method Overloadings. In Sprachen, die es erlauben Operatoren zu überladen, sind Operatoren nämlich eigentlich nur Funktionen, die Symbole wie "+" im Namen verwenden können. Ok, ... außerdem muss noch eine andere Schreibweise für Funktionsaufrufe erlaubt werden: Die Infix-Notation, bei der Parameter Methode Parameter geschrieben wird statt der Präfix-Notation Methode Parameter Parameter
  • Typparameter: Ebenfalls ein Sonderfall des Polymorphimus. Durch Typparameter kann ein Typ, z.B. eine Liste, ad-hoc zur Compilezeit in eine spezialisierte Liste, z.B. für Strings, verwandelt werden. Derselbe Bezeichner "Liste" kann hier so überladen werden, dass er sich im einzelnen Anwendungsfall auf einen oder mehrere bestimmte Typen spezialisiert. Diese werden durch die Typparameter definert. Typparameter sind ein must-have für alle sicheren statischen Typsysteme. (Siehe auch mein anderer Artikel zu Collections.)
  • Implizite Konvertierungen (Type Coercions): Ebenfalls zur Ad-Hoc Polymorphie zählen die impliziten Konvertierungen, wie sie z.B. in vielen Sprachen für numerische Typen gang und gäbe sind. Die Konvertierung ermöglicht es z.B. einen Integer so zu benutzen, als wäre er ein Long-Wert: In noch extremeren Fällen kann es sogar erlaubt sein, einen Integer wie einen String zu benutzen... Auch hier wird also ein Bezeichner mehrfach verwendbar, weil er automatisch in eine passende Rolle "gezwungen" (coerct) wird. Dies ist die einzige Art der Polymorphie, die C beherrscht - und gleichzeitig auch eine sehr fragwürdige, weil sie das Typsystem unsicherer (weak) macht. Typkonvertierungen sind im Allgemeinen Bestandteil der Sprache - es kann aber auch erlaubt sein, dass der Programmierer diese selbst definiert, wie es z.B. in Scala möglich ist.

    2. Dynamischer Polymorphimus (Polymorphismus zur Laufzeit)
    Der dynamische Polymorphismus erlaubt es, erst zur Laufzeit anhand des manifestierten Typs zu entscheiden, was zu tun ist. Insgesamt sind zwei unterschiedliche Strategien bekannt, die als single-dispatch und multi-dispatch bezeichnet werden:
    • Single-Dispatch anhand des Typs (Vererbung, Typklassen)
    Single-Dispatch anhand des Typs ist die verbreitetere Variante. Hierbei ist es möglich, anhand eines abstrakten Typs zu entscheiden, was zu tun ist. Die meisten kennen dies aus der Objektorientierung: Wenn ich auf einer Variablen vom Typ "Set" eine "add"-Methode aufrufe, wird nicht die "add"-Methode von List aufgerufen, sondern von der konkreten Klasse, zu der Wert der Variable gehört. Das kann z:B. ein HashSet oder ein TreeSet sein. Es ist also möglich, durch Abstraktion die Entscheidung auf einen Untertyp zu vertagen, was eigentlich zu ist. Zusätzlich erlaubt es das Konzept der Vererbung, für einige Methoden bereits eine (Default)-Implementierung vorzugeben.
    Auch andere Sprachen, die keine Objektorientierung verwenden, kennen den Single-Dispatch: Haskell verwendet mit seinen Typklassen ein Konzept, das genau das gleiche macht, jedoch auf die starre Objekthierarchie verzichtet. Schaut euch dazu am besten meinen Artikel zu strukturellen Typsystemen an.
    • Multi-Dispatch anhand der Paramaterliste (Multi-Methods)
    Multi-Dispatch ist ein sehr seltenes Phänonem, das eigentlich ausschließlich in dynamischen Sprachen angeboten wird. Da statische Typsysteme sehr stark an Typen und ihren Beziehungen interessiert ist, wird dort traditionell eher der Single-Dispatch verwendet.
    Multi-Dispatch bedeutet dass ich eine Methode zur Laufzeit überladen kann. Dies ist dem Method-Overloading ähnlich, aber flexibler. Wenn ich z.B. eine Multi-Method habe, die drei Parameter entgegen nimmt, dann kann ich dynamisch zur Laufzeit neue Handler definieren, die für eine andere Kombination von Parametertypen gelten. Sobald die Methode mit Werten aufgerufen wird wird dann anhand der Eingangsdatentypen (Klassen) die richtige Methode ausgewählt.
    Das Konstrukt ist deutlich mächtiger als die statische Methodenüberladung und macht primär in dynamischen Sprachen Sinn, die keine Objektorientierung oder Vererbung verwenden (z.B. Clojure).
     
    • Single-Dispatch vs MultiDispatch
    Es ist für eine Sprache sehr unüblich, sowohl single- als auch multidispatch anzubieten, weil beide eine vollkommen andere Herangehensweise haben:
      • Single-Dispatch macht in Systemen Sinn bei denen Typen im Vordergrund stehen, und Abstraktionen anhand der Typschnittstellen realisiert werden. Beispiele: Alle OO-Sprachen, aber auch Haskell.
      • Multi-Dispatch eigent sich für Sprachen, bei denen Operationen im Vordergrund stehen und nicht die Typen. Konsequenterweise wird hier meist auf OO verzichtet und dynamisch typisiert. Beispiele: Lisp-Dialekte, vor allem Clojure.
    Ich werde in meinen Artikel primär den Single-Dispatch betrachten, weil er verbreiteter ist, und ich mich hauptsächlich mit statischen Typsystemen befasse. Für dynamische Sprachen ist Multi-Dispatch oft aber sicher eine bessere Alternative - mir fehlt hierzu nur leider das Wissen und die Praxis.
    • Performance und dynamischer Dispatch
    Das Wort Laufzeit impliziert übrigens auch Kosten: Der dynamische Dispatch ist immer aufwändiger als eine statische Verlinkung von Methoden.
    Bei der Objektorientierung muss z.B. vor jedem virtuellen Methodenaufruf in einer Virtual Method Table nachgeschaut werden, welche Methode denn aufzurufen ist. Diese Kosten können moderne Compiler zu großen Teilen negieren, aber nicht vollständig elimieren. Am effektivsten sind sicherlich Virtual Machines wie die JVM: Diese können zur Laufzeit sicherstellen dass auch wirklich nur die Methoden dynamisch sind, die auch überschrieben werden. In allen anderen Fällen kann die VM den Aufruf zum schnelleren statischem Dispatch optimieren.
    Ad-Hoc-Polymorphismen haben diesen Nachteil nicht: Im Gegenteil können Konstrukte wie Typparameter sogar enorme Performanceverbesserungen bedeuten, da der Compiler den Code mit den präziseren Informationen optimieren kann (aber nicht muss, siehe: Java).


    3. Bewertung der unterschiedlichen Ausprägungen der Polymorphie
    Allgemein kann man sagen, dass Polymorphie gerade für statisch typisierte Sprachen entscheidend sind, um den Code einfacher und generischer zu gestalten. Die wichtigsten Polymorphismen sind definitiv die Typparameter und der dynamische Single-Dispatch.
    Das statische Überladen von Methoden / Operatoren und die impliziten Konvertierungen werden hingegen wohl immer Streitthemen bleiben. Ich würde aber sagen, dass alle ihren Nutzen haben:

    Operatorenüberladung: Alle Sprachen, die Operatoren anbieten, sollten diese auch überladbar machen. Es gibt nichts nervigeres, als wenn die Sprachdesigner meinen, sie wären besser als der Programmierer und dürften Dinge, die er nicht darf. Meist führt dies eher zu schlechterem Code als dies andersherum wäre, weil die eingebauten primitiven Typen dann in Anwendungsfällen vorgezogen werden, in denen man eigentlich selbst passendere Typen definieren sollte.

    Methodenüberladung: Wenn man Operatorenüberladung anbietet sollte man Methodenüberladung im Allgemeinen anbieten. Die Operatorenüberladung ist nämlich eigentlich nur ein Sonderfall der Methodenüberladung.

    Implizite Konvertierungen: Das Streitthema. Implizite Konvertierungen werden meist als unnütz und gefährlich erachtet, und von Freunden der statischen Typisierung als ein Feature von Skriptsprachen verlacht. Das hat eine gewisse Ironie: Tatsächlich bieten meist dynamische "Sprachen" wie PHP exzessive Typumwandlungen an, obwohl diese es gar nicht nötig hätten: Das Duck-Typing macht das Umwandeln von Typen, das man ja primär macht, um Schnittstellen zu erfüllen, nur selten nötig!
    Gerade in objektorientierten Sprachen wären selbstdefinierte implizite Konvertierungen aber sehr nützlich, da hier meist identische Konzepte in unterschiedlichen Objekthierarchien verwendet werden - und die einzige Möglichkeit zum gemeinsamen Verwenden der Hierarchien explizite Typumwandlungen durch Wrapper sind. Implizite Konvertierungen könnten es hier aber ermöglichen, unterschiedliche Hierarchien seamless miteinander zu verbinden!
    Leider ist die einzige Sprache, die bisher diese Möglichkeit bietet, Scala. In allen anderen Sprachen sind die impliziten Konvertierungen schwer verständliche, in die Sprachspezifikation gepresste Features, die nur für bestimmte Datentypen gelten. Allen, die Scalas implizite Konvertierungen noch nicht kennen rate ich, das Thema zu googlen. Es lohnt sich!

    No comments:

    Post a Comment