Monday, November 1, 2010

Imperativ vs Funktional - Krieg der Urgewalten II (gekürzte Fassung)

Hinweis: Den folgenden Artikel zu lesen macht nur dann Sinn, wenn man mit den Gedanken der funktionalen Programmierung vertraut ist:
Grundgedanke dieser Art der Programmierung ist, dass eine Funktion niemals ihre Eingangswerte verändern darf, sondern nur einen (neuen) Ausgangswert zurückliefern darf. Dieses akademische Konzept widerspricht dem weitverbreiteten imperativen Paradigma, das ein Programm komplett als eine zustandsbehaftete Abfolge von Befehlen sieht. (z.B. C, Pascal, Java). Beispiele für Sprachen mit einer funktionalen Orientierung sind Haskell, Clojure und teilweise Scala . 

Der Flamewar der beiden Lager der funktionalen und der imperativen Programmierung wird ähnlich erbittert geführt wie der zwischen den dynamischen und den statischen Sprachen. Oft genug scheint es die Praxis zu sein, dass funktionale und imperative Ansätze als konkurrierend betrachtet werden. Warum? Beide Ansätze haben ihre Vorzüge und Einsatzszenarien!

Imperativ
  • Entscheidend für echte Prozesse und Workflows.
  • Performanceoptimierungen sind am besten mit imperativen Konzepten möglich, da im Endeffekt auch jeder funktionaler Code spätestens durch den Compiler in eine imperative maschinenlesbare Form gebracht werden muss.
  • Leicht verständlich, so lange der Scope der Seiteneffekte beschränkt bleibt. Im privaten Scope von Objekten ist imperatives Denken z.B. sehr gut nachvollziehbar und meist auch leicht zu verstehen.
  • Stark imperative Ansätze, d.h. solche die die Eingangsparameter von Methoden verändern, statt neue Ergebnisse nur über die Ausgabeparameter zu liefern, führen schnell zu schwer verständlichen und schwer wartbarem Code. Im schlimmsten Fall muss man eine Kette von Funktionsaufrufen zum Ursprung zurückverfolgen, um herauszufinden wann wie wo ein Wert verändert wurde.
  • Imperative, veränderliche Objekte sind nicht teilbar. Sollen sie in unterschiedlichen Kontexten verwendet werden, wird man immer gezwungen sein, eine (defensive) Kopie des Werts anzulegen - oder schwer auffindbare Bugs heraufbeschwören.
  • Der Ansatz ist nicht threadsicher. Imperatives Denken funktioniert nur in deterministischen Scopes. Threads aber sind von ihrem Naturell her nichtdeterministisch.
  • Für die sichere Fehlerbehandlung bei Methoden ohne Rückgabewert können nur Exceptions verwendet werden - ein Konzept das nur in Java wirklich sicher ist, da es als einzige Sprache "checked" Exceptions kennt. Der Grund dafür ist dass man in allen anderen Fällen wie z.B. bei C nicht gezwungen ist, den Rückgabewert einer Funktion z.B. den Fehlercode zu prüfen. Nur Exceptions erzwingen eine Behandlung durch den Programmierer.

Funktional
  • Die Schnittstellen sind viel leichter verständlich: Eingangswerte können / werden nicht verändert, Ausgangswerte stellen (evtl. neue) Ergebnisse dar.
  • Funktionale Methoden sind test- und wartbarer: Sie beruhen niemals auf externen Zuständen und beeinflussen diese auch nicht. Dadurch sind sie auch generisch in unterschiedlichen Kontexten einsetzbar.
  • Funktionen sind gleichzeitig Werte, d.h. jede Funktion ist eine Closure die auch an andere Funktionen übergeben werden kann. Dadurch lassen sich Funktionen höherer Ordnung bauen, z.B. eigene for-each-Konstrukte.
  • Funktionale, immutable Objekte können beliebig geteilt werden, wodurch oft auf das Anlegen neuer Instanzen verzichtet werden kann. Das gleicht den Nachteil wieder aus, das man grundsätzlich eher mehr Objekte erzeugen muss als in imperativen Kontexten.
  • Ohne bestimmte Sprachstrukturen nicht durchführbar: Closures und Tupel müssen in der Sprache first class values sein. Zu Tupeln - mehrfachen Rückgabewerten - folgt bezeiten noch ein Artikel. Sonst einfach mal googlen.
  • Falls ein statisches Typsystem eingesetzt werden muss, sollte dies nach Möglichkeit strukturell sein um Typinferenz zu ermöglich. Soll die Sprache auch objektorientiert sein, dann kann das objektorientierte Denken in Klassen sehr leicht als Submenge des strukturellen Typsystems realisiert werden.

Man kann, unter Berücksichtigung einiger Bedingungen, beide Ansätze leicht vermischen. Da funktionale Programmierung immer nur eine Teilmenge sein kann, ist dies sicherlich ein vernünftiger Ansatz. Einige Dinge sind nur imperativ zu lösen, und jede Implementierung ist am Ende immer imperativ.

In der Praxis trifft man die beiden Ansätzen in Sprachen meist in der folgenden Form:
  • Hauptarten
    • Rein imperative Sprachen - in modernen Varianten meist mit "Objektorientierung". Oft sind keine Closures verfügbar, sie können es aber sein. (Java, C#, C++).
    • Rein funktionale Sprachen. Imperative Abläufe können nicht direkt in der Sprache ausgedrückt werden, sondern werden über ein besonderes Konstrukt ermöglicht, dass die Imperative Änderung funktional beschreibt: Die Monade. (Haskell)
  • Mischtypen
    • Eine funktionale Sprache mit minimalen imperativen Elementen. Veränderliche, imperative Konstrukte können trotz der funktionalen Orientierung mit geringem Mehraufwand eingebunden werden, werden aber nach Möglichkeit discouraged. (Clojure).
    • Eine imperative Sprache, die jedoch einen sehr starken Fokus auf den funktionalen Ansätzen hat und diese auch bevorzugt. (Scala, interessanterweise aber auch Javascript).

No comments:

Post a Comment