Sunday, October 31, 2010

Dynamisch Vs. Statisch - Der Krieg der Urgewalten

Dieser Artikel beschäftigt sich mit einem der ältesten Themen der Softwareentwicklung - und einen in dem es die heftigsten Grabenkämpfe gibt: Statische Typprüfung oder doch lieber dynamische Typisierung?

Ziel soll ein Vergleich zwischen den Unterschieden der Systeme sein, nicht der Sprachen und ihrer Features.

Dynamisch typisiert ist nicht gleich dynamisch typisiert, man vergleiche nur PHP, Phyton und Clojure.... Bei den statischen Typsystemen fallen die Unterschiede sogar noch viel größer aus, weil diese viel mehr theoretische Substanz und ein viel aufwendigeres Design für das Typsystem und seine Einbindung enthalten müssen.

Dazu auch gleich der erste Punkt:
Viel Blame an den statischen Typsystemen ist meist eher Blame an einzelnen Sprachen und deren schlechter Umsetzung desselben.

Im Falle Java hat die Unzufriedenheit mit der Sprache soweit geführt, dass die dynamischen Sprachen sogar bei Entwicklern beliebter geworden sind, die früher vllt. statische Typsysteme favorisiert haben. Einer der Hauptgründe für die Unbeliebtheit vieler statischer Sprachen dürfte die fehlende Typinferenz sein, d.h dem Zwang an allen Variablen auch immer die Typen mitanzugeben.

Dies ist jedoch eigentlich nur eine Designschwäche:
Statische Sprachen wie Scala, OCaml oder Haskell, aber auch C# kommen an vielen Fällen ohne Typannotationen aus und verwenden kurze Schlüsselwörter, z.B. var oder let, zur Variablendefinition, ohne Angabe des Typs. Der Code ähnelt dann dem von dynamischen Sprachen und ist ähnlich kurz. Mehr zum Thema Typinferenz in einem späteren Artikel...

Einer der häufig genannten Sellingpoints statischer Sprachen ist nach der Meinung ihrer Evangelisten die Performance, die höhere Sicherheit zur Laufzeit und die bessere Dokumentation des Codes. Außerdem sieht man durch die Typannotationen besser was passiert. Alle diese Punkte sind valide, allerdings zu recht für viele Freunde dynamischer Sprachen nicht stichhaltig genug:
  • Performance ist relativ: Man hat bei dynamischen Sprachen zwangsläufig höhere Kosten durch den dynamischen Dispatch aller Methodenaufrufe. Es gibt hier aber Fortschritte, durch die diese Kosten in einen marginalen Bereich zurückgedrängt werden können. Auch dynamische Sprachen können z.T. den schnelleren statischen Dispatch einsetzen, wenn der Compiler in der Lage ist, die Datentypen aus dem Kontext heraus zu identifizieren(=> Typinferenz). Einige Sprachen, z.B. Clojure, bieten auch die Möglichkeit, Typen explizit zu annotieren und auf diese Art und Weise an Schlüsselstellen statische Performance zu bieten.
  • Typsicherheit: Setzt voraus, dass das Typsystem so umfassend ist dass es möglich ist, damit typsicher zu arbeiten. Das ist aber selten der Fall, eigentlich ist es nur in so mächtigen Typsystemen wie dem von Haskell wirklich möglich. Merke: Jeder Typecast öffnet genau die Lücke, die statische Sprachen eigentlich schließen wollen. Ergo ist Code mit vielen Typecasts ein klarer Sellingpoint für dynamische Sprachen - oder Sprahcen mit besseren Typsystemen. Einen wirklichen Verzicht auf Typecasts wird man wohl nur mit strukturellen Typsystemen erreichen (halt Haskell oder Scala).
  • Bessere Codedokumentation durch Typannotationen: Korrekt. Allerdings sollte die Sprache Typannotationen nur bei Methoden für vorraussetzen, weil nur dort wirklich wichtig ist, welche Typen erwartet werden.
  • Einfacheres Refactoring / Bessere Toolunterstützung: Finde ich sehr wichtig. Statische Sprachen haben es leichter, dem Entwickler Vorschläge zu machen, welche Methoden er aufrufen kann etc. Bei dynamischen Sprachen fehlt oft diese Hilfestellung, weil er Compiler selbst nie alle Funktionen kennen kann, die aufrufbar sind. Dennoch wird es auch hier Fortschritte geben, so dass dieses Argument nur bedingt tauglich ist.
Evangelisten dynamischer Sprachen können außerdem auf klare Vorteile ihres Ansatzes hinweisen:
  • Fehler bei der Typisierung sind eher selten und gehören meist nicht zu den Showstoppern. Korrekt. Wäre dies so, dann gäbe es ja gar keine dynamischen Sprachen. Außerdem bedeutet statische Typsicherheit ja nicht, dass der Code das macht was er soll, sondern nur dass alle Methoden und Typen richtig aufgelöst werden können.
  • Wirklich sicheren Code erreicht man durch Unittests. Auch korrekt, aber man muss theoretisch auch viel mehr testen - nämlich auch dass die Typ-Constraints erfüllt werden. In der Praxis wird man dieses aber nur selten tun. Dennoch: Bei Test-First-Ansätzen ist der Vorteil der Typsicherheit statischer Sprachen zu vernachlässigen, weil der Code hier nur Mittel für einen gesetzen Zweck ist.
  • Keine Typannotationen - Ist wie oben gesagt eine Schwäche vieler statischer Sprachen, aber nicht der statischen Typisierung an sich.
  • Duck-Typing ermöglicht es, Code mit wenig Redundanzen zu schreiben. Der Code legt den Fokus stärker auf Prozesse und Schnittstellen und weniger auf Typhierarchien. Finde ich valide, allerdings ermöglichen strukturelle Typsysteme fast dasselbe. Man muss aber fairerweise sagen, dass man auch nicht alle Spielarten der dynamischen Typisierung durch ein strukturelles Typsystem abbilden kann.

Zwischenstand

Statisch
  • Je stärker das Typsystem, desto mehr machen sich die Vorteile einer statischen Sprache bezahlt. Inbesondere dann wenn nur Single-Dispatch angeboten wird (d.h.: Dispatch auf der Basis von Typen => Vererbung) ist statische Typisierung sehr gut implementierbar.
  • Typinferenz und strukturelle Typsierung sind die Schlüsseleigenschaften zu einfachem Code
  • Objektorientierung und nominelle Typisierung sind nützlich, aber nicht unbedingt erforderlich. Falls aber OO verwendet wird, ist eine klare Objekthierarchie und der Ansatz Everything Is An Object das A und O zur einfacher Verwendung. Nicht nur C++ ist hier ein Gegenbeispiel, sondern auch Java mit seinen Primitiven und der  z.T. etwas eigenwilligen Vererbungshierarchie im JDK.
  • Casts sind der Feind der statischen Typisierung! Wenn häufig Casts nötig werden, oder der Programmierer sie für nötig hält, dann sollte er besser zu einer dynamischen Sprache wechseln. Die Vorteile der statischen Typisierung sind dann nämlich dahin. 
  • Besserer IDE-Support durch das Typsystem möglich
  • Es kann eine bessere Performance erreicht werden
Dynamisch
  • Das Typsystem und seine Struktur treten in den Hintergrund. Der Programmierer kann sich auf Operationen konzentrieren und muss weniger über Typsysteme wissen.
  • Automatische Typkonvertierungen (Koercions) sind in dynamischen Systemen extrem gefährlich. Aufgrund der großzügigeren Typprüfung kann der Code schnell nicht mehr nachvollziehbar werden, wenn zusätzlich noch implizite Konvertierungen vorgenommen werden. Mit einer starken Typisierung ist ein dynamisches Typsystem aber relativ leicht beherrschbar, wie z.B. in Phyton. Gegenbeispiele sind PHP und Javascript.
  • Lispartige Sprachen, d.h. Sprachen die in ihrer eigenen Syntax entwickelt sind, sind fast zwangsläufig von dynamischer Natur. Will man also eine Homophone Sprache verwenden, wird man aus Gründen der Einfachheit immer dynamisch typisieren wollen (Clojure).
  • Der Aufwand für Unittests ist durch die fehlende Sicherheit höher, ebenso die Unterstützung durch IDEs geringer. Der Vorteil, sich stärker auf die Prozesse konzentrieren zu können, ist aber sehr groß.
  • Die Sprache wird einfacher durch die geringe Bedeutung des Typsystems. Flexiblere Ausdrücke sind möglich. Das ermöglicht eine leichtere Implementation und ein schnelleres Verständnis der Sprache. Man vergleiche nur den Umfang eines Clojure-Lernbuchs mit einem Java- oder Scala-Buch...
  • Meiner Meinung nach können nur dynamische Sprachen sinnvollen "Multi-Dispatch" in der Form von Multimethods anbieten.
Und nun? Grundsätzlich kann man es fairerweise so zusammenfassen: Eigentlich gibt es kein schlechter und kein besser. (Ja, ich weiß... laaaangweilige Antwort).

Programmierer, die gut im Kategorisieren und Definieren von Wertmengen sind, werden ihre Produktivität mit statischen Sprachen besser entfalten können. Wenn man eine "perfekte" Lösung erstellt hat, ist meist die Motivation zu Tests geringer - eine wohltypsierte Lösung möchte man wahrscheinlich kaum durch redundante Tests noch mal "beweisen". Da kommt es sicher entgegen, dass die statischen Typsysteme einem diese Aufgabe zu einem gewissen Teil bereits abnehmen.

Programmier, die hingegen prozessorientiert Denken mögen gegenteilige Erfahrungen gemacht haben: Gerade der Ansatz "Test First" ist dann am effektivsten, wenn man mit dynamischen Sprachen arbeitet: Hier werden zuerst die Tests, also die Validierungsroutinen entwickelt und darauf aufbauend eine möglichst einfache Implementation erzeugt, die passt. Sowohl die Definition als auch die Durchführung dieses Vorgangs ist mit dynamischen Sprachen und ihrer stärker auf Ergebnisse ausgelegten "Denkweise" besser möglich. Dafür sind die Ausdrücke dann meist weniger gut in statischen Typsystemen ausdrückbar. Es funktioniert oder es funktioniert nicht - warum ist für das Erreichen eines Ergebnisses ja eigentlich nicht so wichtig.

Ich persönliche tendiere eher zu ersten Sichtweise, halte die zweite aber auch für sehr sinnvoll, und in der Praxis auch leichter anwendbar. Was meiner Meinung nach für beide Sichtweisen gilt ist: Die Sprache muss gut sein.
Das gilt vor allem für die statischen Sprachen! Java, C/C++, ... , alle sind unnötig kompliziert, haben unzureichende Typsysteme mit seltsamen Sonderlocken für Arrays und Strings etc. Da lohnt sich auch für Analytiker meist kaum der Einarbeitungsaufwand. Zum Teil gibt es noch nicht mal die Möglichkeit, Closures zu definieren. Außerdem fehlt vor allem Java und C++ immer noch die Typinferenz, so dass der Code sehr aufwendig zu erstellen und zu pflegen ist.
Vor dem Hintergrund wäre man eigentlich verrückt, die geringen Vorteile einer solchen statischen Sprache den eindeutigen einer dynamischen vorzuziehen. Diese sind nämlich schon allein von ihrem Naturell her einfacher zu entwickeln, da das Typsystem ja egal ist. Abgesehen von hässlichen Kröten wie PHP ist man deshalb auch in der Lage, mit den meisten Sprachen recht ähnliche Ergebnisse zu erreichen und ähnlich zu entwickeln.
Dennoch sehe ich auch in diesem Lager Unterschiede. Insbesondere wundert mich, das viele dynamische Sprachen dennoch objektorientiert sind. Grundsätzlich ist OO ok, aber sie ist im Rahmen von statischen Typsystemen viel nützlicher als in den dynamischeren Umgebungen, denke ich. Dynamische Sprachen sollten sich stärker auf Funktionen als Typen und Klassen konzentrieren!

Meine vollkommen subjektive Meinung
  • Wenn statisch, dann etwas in der Richtung von Scala oder Haskell ("echtes Typsystem", strukturell, Typinferenz). Gerne auch objektorientiert.
  • Wenn dynamisch, dann kann man den Vorteil der Homophonie nutzen: Die Sprache ist in ihrer eigenen Syntax entworfen. Aufbauend auf einer geringen Syntax werden alle Kontrollstrukturen und alle Konstrukte wie z.B. Funktionen ausgedrück, Da bietet sich natürlich die Lisp-Familie mit Clojure an..
  • Ein weiterer sehr starker Contender für dynamische Sprachen ist hier durch seine Ausgereiftheit und sein solides Wachstum Python. Warum? Weil es einfach eine schöne Sprache ist. ;-)

    No comments:

    Post a Comment