Funktionales Programmieren: das vernachlässigte Paradigma
Einleitung
Programmieren ist zu einem wesentlichen Teil Abstrahieren von konkreten
Zuständen und Abläufen. „In Code gießen“ ist eine oft gehörte Redewendung
für genau diesen Prozess. In Einführungen zur Objektorientierung in Sprachen
wie Java muss die Klasse Car
mit ihrer Eigenschaft
wheels
als stets wiederkehrendes Beispiel herhalten.
Sie bietet aber die Möglichkeit, das Konzept
Automobil im Programm zu verwenden und den Anforderungen
anzupassen. Programme für Fertigungsmaschinen benötigen andere
Methoden als die Buchhaltungssoftware eines Autohauses.
Beteiligte Entwickler haben bei gut
gewählten Klassen eine intuitive Ahnung, was diese bezwecken oder darstellen
sollen.
Dass sie dabei unter dem Constructor einer Car
-Instanz etwas
vollkommen anderes verstehen als der Maschinenbauer, der die Software
einsetzt, verringert deshalb nicht die Ausdruckskraft des Objekts.
Was wäre, wenn neben Objekten eine weitere kraftvolle Art der Abstraktion in JavaScript nur darauf wartete, eingesetzt zu werden? Brendan Eich war seinerzeit mit dem Versprechen von Netscape geködert worden, Scheme in den Browser bringen zu können. Scheme, eine Lisp-Variante, unterstützt das funktionale Programmier-Paradigma, das schon seit langem von seinen Anhängern für seine Flexibilität geschätzt wird. Das Ergebnis war JavaScript. Funktionale Programmierung (FP) ist dadurch ein Kernbestandteil des JavaScript-Werkzeugkastens. Manche ihrer Pattern werden bereits häufig eingesetzt, ohne dass man sich dessen bewusst ist.
Ein solides Fundament in vielen Sprachen
In C und anderen imperativen Sprachen gibt es eine mehr oder weniger strikte konzeptuelle Trennung zwischen Werten und Funktionen. Funktionale Programmiersprachen behandeln Funktionen dagegen als „Bürger erster Klasse“, die im Wesentlichen all das können, was andere Werte können, z. B. an Variablen zugewiesen werden.
In der Computertheorie sind funktionale Sprachen solche, die auf dem
λ-(Lambda-)Kalkül basieren. Ende der 50er Jahre war Lisp die erste Sprache,
die vieles aus dieser Theorie umsetzte. Auch wenn Lisp-Code berüchtigt ist für die Menge an runden
Klammern, die in der Syntax vorkommen, wird die Sprache an sich oft als eine
der elegantesten für viele Problemstellungen beschrieben. Die Idee und
praktische Umsetzung der FP sind also keinesfalls brandneu, sondern
wohlgetestet und älter als C. Das λ-Kalkül modelliert das Konzept von
Funktionen so, dass sie mit formalen Methoden untersucht und beschrieben
werden können. Dadurch ist eine theoretische Behandlung ihrer Eigenschaften,
z. B. im Rahmen der Logik, möglich. Ausgehend u. a. von der Beschreibung der
einfachen Anwendung einer Funktion, der sogenannten β-Reduktion, baut das λ-Kalkül das
Fundament für alle in diesem Artikel erwähnten Techniken. Dabei ist
die Formulierung für erfahrene Programmierer sehr intuitiv. Die β-Reduktion
sagt zuerst einmal nichts anderes aus, als dass ein Funktionsaufruf der
Funktion F()
mit Parameter x
durch den
Rückgabewert F(x)
ersetzt werden kann, ein Vorgang, den jeder
Software-Autor täglich dutzendfach verwendet.
Ein kleines Beispiel dazu: Eine Funktion, die den Wert der Eingabevariable verdoppelt, sieht in JavaScript so aus:
function verdopple(x) { return x + x; }
Das λ-Kalkül verwendet für die gleiche Prozedur die Schreibweise
verdopple = λx.x + x
. Will man die Funktion für den Wert
5
ausführen, setzt man ihn im Inneren des λ-Ausdrucks für alle x
ein:
verdopple(5) = (λx.x + x)(5) = 5 + 5 = 10
Für alle folgenden Betrachtungen kann dann verdopple(5)
durch 10
ersetzt werden. Deshalb wird der Begriff
Reduktion verwendet, weil der Funktionsaufruf zu einem einfachen
Wert reduziert werden kann.
Die ganze Bandbreite funktionaler Werkzeuge wird nicht von jeder Sprache vollständig unterstützt. In JavaScript, ebenso wie in Lisp, Python und vielen anderen, werden nur einzelne Elemente umgesetzt, soweit es der Autor der Sprache für sinnvoll erachtete. Einige Beispiele werden weiter unten gezeigt. Andere Sprachen wiederum verpflichten sich vollständig dem funktionalen Paradigma einschließlich aller Einschränkungen. Programmieren in diesen Sprachen nennt man auch reine FP.
Joel Spolsky, einer der Köpfe hinter StackOverflow, erklärt, warum die Beschäftigung mit funktionaler Programmierung so wichtig ist:
Without understanding functional programming, you can’t invent MapReduce, the algorithm that makes Google so massively scalable. The terms Map and Reduce come from Lisp and functional programming.
Ohne funktionales Programmieren zu verstehen, kann man MapReduce nicht erfinden, den Algorithmus, der Google so massiv skalierbar macht. Die Begriffe Map und Reduce stammen von Lisp und funktionaler Programmierung.
Er legt dar, wie wichtig es für die alltägliche Arbeit eines Programmierers ist, in einer Sprache auf unterschiedlichen Abstraktionsebenen arbeiten zu können. Die Existenz von Funktionen stellt eine solche Abstraktionsebene dar, und die Fähigkeit, Funktionen frei verwenden zu können, ist eine wichtige weitere.
Programmierer, die bislang stets mit imperativen Sprachen arbeiteten,
werden von einer Eigenschaft rein funktionaler Programmierung meistens
vollkommen überrascht: nicht veränderbaren Variablen.
x = x + 1
ist, im Gegensatz zu C, Java oder auch JavaScript, in
den rein funktionalen Sprachen Haskell oder Erlang nicht möglich. Es ist eine
erhebliche gedankliche Umstellung, die hier vom funktionalen Programmieren
gefordert wird. Tritt man aber einen Schritt zurück, stellt man fest, dass
aus mathematischer Sicht obige „Gleichung“ ziemlich nutzlos und nur in der
überschaubaren Anzahl Strukturen lösbar ist, in denen 0 = 1 gilt. Dadurch, dass
diese erneute Wertzuweisung verboten ist, wird gleichzeitig einer ganzen
Klasse von Fehlern ein Riegel vorgeschoben und die theoretische Behandlung
des Codes auf solide mathematische Füße gestellt.
Diese Art des Programmierens hat deshalb einen ganz besonderen Vorteil: Sie ist nebenwirkungsfrei. Was heißt das? Wenn eine Funktion immer wieder mit den gleichen Parametern aufgerufen wird, wird sie stets den gleichen Wert zurück liefern. Die Bugfreiheit eines Programms in einer rein funktionalen Programmiersprache kann so sogar mathematisch bewiesen werden, auch wenn das schnell zu komplex wird, um in der Praxis von Relevanz zu sein. Zudem ist Parallelisierung besonders einfach, weil einzelne Codeteile nicht auf Zustände in anderen Teilen des Programms Rücksicht nehmen müssen. Threads in anderen Programmiersprachen, die zu unterschiedlichen Zeiten auf ein und dasselbe Objekt zugreifen und dabei seinen internen Zustand heillos durcheinander wirbeln, sind oft genug Quellen schwer identifizierbarer Bugs. Dieses Problem wird in reiner FP dadurch umgangen, dass es gar keinen internen Zustand irgendwelcher Objekte gibt, sondern alle Operationen ausschließlich auf den festgelegten derzeitigen Variablen der Umgebung fußen.
Viele Möglichkeiten funktionalen Programmierens ziehen in immer mehr „klassische“ Sprachen ein. Python, PHP und Ruby erhalten mit jedem Release mehr und mehr funktionale Features. Und auch an Orten, an denen man es nicht ohne weiteres erwartet, ist funktionales Programmieren vorhanden: XSLT-Stylesheets sind rein funktionale Programme, und auch Makefiles können als solche gesehen werden. In der .NET-Welt wurde F#, und in der Java-Welt Scala und Clojure eingeführt, die nicht umsonst wesentliche Bestandteile der FP umsetzen.
Schließlich verwenden mittlerweile so manche Projekte funktionale Sprachen unter der Haube, ohne dass es auf den ersten Blick ersichtlich ist. CouchDB ist z. B. in Erlang geschrieben. Auf Serverseite werden neben Unternehmensanwendungen auch Experimente für kleine Webprojekte in Haskell durchgeführt.
Wie sieht Funktionales Programmieren aus?
Charakteristisch für funktionale Sprachen ist, dass sie das Augenmerk darauf legen, was der Rechner tun soll. Imperative Sprachen sagen ihm dagegen eher, wie er etwas zu tun hat. Bei den ungewöhnlicheren Vertretern der FP sieht man das recht gut. XSLT-Stylesheets, die eine XML-Sprache in eine andere umwandeln, definieren nur die Struktur der Ausgangs- und Zielsprache. Wie der XSLT-Interpreter die beiden XML-Dokumente behandelt, ob mit DOM, SAX, Expat oder regulären Ausdrücken, ist für das Bearbeiten von XSLT-Stylesheets vollkommen nebensächlich.
Dadurch, dass man hauptsächlich beschreibt, was mit den einzelnen Elementen passiert, und nicht näher spezifiziert, wie man ein konkretes Element anspricht, wird der Code für die Aufgabe „transformiere ein XML-Dokument in ein anderes“ schlanker, übersichtlicher und leichter wiederverwendbar. Die meisten funktionalen Sprachen setzen freilich keine vollständige Maskierung der Prozedur, des Was, ein. Trotzdem fördert funktionales Programmieren aufgaben-fokusierten Code. Sehen wir uns jetzt an, wie funktionales Programmieren in praktischem JavaScript aussehen kann. Man findet sich oft in einer Situation, in der das Zwischenspeichern eines Zustandes, eines state, notwendig ist. Nehmen wir als Beispiel eine einfache Aufklapp-Box:
<p><a href="#" id="Öffner">öffne Box</a></p> <div id="box" style="display: none"> <p>Lorem ipsum dolor sit...</p> </div>
Und folgende JavaScript-Umsetzung der Klappfunktion:
var zustand = 'geschlossen'; var box = document.getElementById('box'); function aendere() { if (zustand === 'offen') { box.style.display = 'none'; zustand = 'geschlossen'; } else { box.style.display = 'block'; zustand = 'offen'; } }
Ein Beitrag zum „Umweltschutz“
Dieser Ansatz ist nicht sinnvoll, wenn wir mehr als eine Box haben. Wir brauchen eine Möglichkeit, den Zustand pro Element zu speichern.
function erzeugeBox(box) { var zustand = 'geschlossen'; function aendere() { if (zustand === 'offen') { box.style.display = 'none'; zustand = 'geschlossen'; } else { box.style.display = 'block'; zustand = 'offen'; } } return aendere; } var aendereFAQ = erzeugeBox(document.getElementById('FAQ'));
Dadurch, dass wir in aendereFAQ
eine Referenz auf
aendere
speichern, haben wir ein Closure erzeugt:
Variablen werden in einer Funktion konserviert, auch wenn die Umgebung, in
der sie definiert wurden, nicht mehr existiert. In diesem Fall werden
zustand
und box
vom Garbage Collector nicht
entfernt, auch wenn erzeugeBox
bereits beendet ist. Solange es
noch eine bestehende Referenz auf aendere
gibt, existieren sie
weiter.
Man kann es sich bildlich besser vorstellen, wenn man beachtet, in
welcher Umgebung die Funktion aendere
definiert wurde.
Eigentlich ist sie nur im Bereich von erzeugeBox
definiert. Die
Elemente in ihrer Umgebung bleiben aber erhalten, weil aendere
diesen Bereich mittels der return
-Anweisung verlässt.
Closures haben besondere Beliebtheit erlangt, weil sie erlauben, dauerhafte Variablen zu definieren, ohne den globalen Namensraum mit dutzenden temporären Variablendefinitionen zu strapazieren. Dadurch kann man Namenskollisionen recht effektiv vermeiden. Viele Anleitungen zum Entwickeln von JavaScript-Modulen beginnen damit, dass der Code in eine sofort ausgeführte Funktion, englisch Immediately Invoked Function Expression oder kurz IIFE genannt, gekapselt werden soll:
(function() { var meine_variable; ... return interface_nach_aussen; })(); console.log(typeof meine_variable); // undefined
Das dient einzig dazu, die Freiheit zu haben, intern Variablen nach
Herzenslust zu definieren. Diese Variablen sind von dem umgebenden Code aus
nicht zu erreichen, so wie private Eigenschaften in Java-Objekten. Das
Closure, das zurückgegeben werden kann, dient dann als Interface nach außen.
(In vielen Fällen wird es in der IIFE einfach an ein globales Objekt wie
window
angehängt, statt zurückgegeben.)
Eine andere Eigenschaft funktionaler Programmierung haben wir in Zeile 13 wie selbstverständlich verwendet, und sie macht Closures erst möglich. Funktionen können wie Variablen behandelt werden. Sie können Rückgabewerte anderer Funktionen sein oder als Parameter übergeben werden. Sie können anderen Variablen als Wert zugewiesen werden und brauchen nicht mal einen Namen. In letzterem Fall spricht man auch von anonymen Funktionen. Alle Funktionen in JavaScript erfüllen diese Bedingungen.
Die Wrapperfunktion erzeugeBox
gibt die Funktion
aendere
zurück. Wir können auf den Namen verzichten und sie
anonym machen.
function erzeugeBox(box) { var zustand = 'geschlossen'; return function() { if (zustand === 'offen') { box.style.display = 'none'; zustand = 'geschlossen'; } else { box.style.display = 'block'; zustand = 'offen'; } } } var aendereFAQ = erzeugeBox(document.getElementById('FAQ'));
Das Beispiel ist identisch zu dem vorherigen. Aber durch den Wegfall des
unnötigen Funktionsnamens wird es sowohl kompakter als auch leserlicher.
Eine Funktion wie erzeugeBox
, die andere Funktionen zurückgibt,
nennt man Funktion höherer Ordnung.
Wir müssen noch dafür sorgen, dass aendereFAQ
bei einem
Klick auf ein Öffner-Element ausgeführt wird.
var oeffner = document.getElementById('FAQÖffner'); oeffner.addEventListener('click', aendereFAQ, false);
Die Methode addEventListener
erwartet als zweiten Parameter
eine Funktion. Sie ist ebenfalls eine Funktion höherer Ordnung.
Kleiner Blick über den Tellerrand
Dieses Konzept der Funktionen höherer Ordnung hat es seit Version 5.3
sogar in PHP geschafft. Davor musste man für Funktionsübergaben und anonyme
Funktionen hässliche Konstrukte wie create_function
bemühen,
die aus Strings mit einer eval
-artigen Methode schwer debugbare
Funktionen erzeugen. Mittlerweile sieht PHP so aus:
<?php $greet = function($name) { printf("Hello %s", $name); };
In Python sind Funktionen höherer Ordnung so beliebt geworden, dass dafür
eigens die
Decorator-Syntax eingeführt wurde. Die Notation mit dem @
:
def aussen(f): print "Hallo Welt" return f @aussen def innen(): return True
ist das gleiche wie
def innen(): return True innen = aussen(innen)
Teilen und herrschen
Es kann natürlich vorkommen, dass eine Box bereits geöffnet ist. Wir
brauchen deshalb einen Weg, auch den Anfangszustand mit zu übergeben, am
besten, ohne die Verwendung des bisherigen erzeugeBox
zu
beeinflussen.
function erzeugeZustandsbox(zustand, box) { return function() { if (zustand === 'offen') { box.style.display = 'none'; zustand = 'geschlossen'; } else { box.style.display = 'block'; zustand = 'offen'; } } } function erzeugeBox(box) { return erzeugeZustandsbox('geschlossen', box); };
Was geschieht hier? Wir haben erzeugeBox
umdefiniert, so
dass es das generische erzeugeZustandsbox
mit einem fest
definierten Zustand 'geschlossen'
aufruft und das Ergebnis
zurückgibt. Von außen ist es nicht von der vorherigen Version zu
unterscheiden. Wir haben erzeugeZustandsbox
partiell
angewendet. Die Funktion erzeugeBox
benötigt nur einen
Parameter, weil sie bereits weiß, wie der zweite Parameter für
erzeugeZustandsbox
lauten muss. Von partieller Anwendung
spricht man allgemein, wenn eine Funktion mit n Variablen so
verändert wird, dass sie mit weniger Variablen aufgerufen werden kann.
Neuere Browser spendieren dem Function
-Prototyp für diesen
Anwendungsfall die bind
-Methode.
Sie bietet eine einfache Syntax, um beliebige Funktionen partiell
anzuwenden. Der erste Parameter definiert den Wert von this
,
alle weiteren werden als Argumente der Funktion vorausgefüllt.
var erzeugeBox = erzeugeZustandsbox.bind(undefined, "geschlossen");
Funktionale Werkzeuge einsetzen
Das Verändern bestehender Funktionen, wie z. B. die partielle Anwendung
von erzeugeZustandsbox
in erzeugeBox
, kann man
analog zur Vererbung in OOP-Klassen verwenden. Soll eine bestehende
Basisfunktion nur ein wenig verändert werden, übergibt man sie einer
Funktion, die diese Änderung durchführt. Man verwendet in Zukunft
die von letzterer zurückgegebene Funktion. Grundlegende Funktionalität wird
in beiden Fällen von dem Basisobjekt bzw. der Basisfunktion bereit gestellt.
Die Erweiterungen dienen der Anpassung an den Einzelfall. Im Gegensatz zur
Objektorientierung, die Vererbung von sich aus unterstützt, muss in FP die
Funktion zur Vererbung erst geschrieben werden. Das kann auch von
Vorteil sein, wenn man Details der Vererbung programmatisch anpassen möchte.
Ein zweiter Workflow ist so machbar. Eine mächtige Basisfunktion
erwartet als Parameter eine optionale Funktion. Dadurch passt man die
Basisfunktion dann an unterschiedliche Bedürfnisse an. Der Vorteil ist hier,
dass die Basisfunktion passgenau die gewünschte Funktionalität verwenden
kann. Die Klassenvererbung verwendet für gewisse Szenarien Syntaxelemente,
z. B. Schlüsselwörter wie final
, um Vererbung zu verhindern. Die
beiden Vorgehensweisen, Modifikation einer Funktion „von außen“ oder „von
innen“, lösen ähnliche Probleme rein programmatisch.
Werte aus Arrays: reduce
Wenn wir die Anzahl geöffneter Boxen bestimmen wollen, kann man das z. B. so machen:
var offeneBoxen = 0; for (var i = 0; i < alleBoxen.length; i++) { if (alleBoxen[i].style.display === 'block') { offeneBoxen += 1; } }
Es wäre schön, wenn wir eine einfache Möglichkeit hätten, aus einem
beliebigen Set (Array, Elemente, …) einen Wert nach einer bestimmten Methode
zu gewinnen. Diese Möglichkeit gibt es. Ihre Umsetzung ist denkbar kurz und wird
üblicherweise als reduce
bezeichnet.
function reduce(array, start, combine) { for (var i = 0, j = array.length; i < j; i++) { start = combine(start, array[i]); } return start; } var offeneBoxen = reduce(alleBoxen, 0, function(start, value) { if (value.style.display === 'block') { start += 1; } return start; });
Unser Code ist nicht wesentlich länger geworden, aber wir haben an
Flexibilität gewonnen. Durch die Abstraktion haben wir zudem einen weiteren
Vorteil. Nehmen wir an, wir stellen fest, dass wir statt Knotensets, wie sie
z. B. von getElementsByTagName
zurückgeliefert werden,
tatsächliche Arrays benötigen. Im ersten Fall müssen wir durch alle
for
-Schleifen im Code gehen und die Knotensets an jedem Punkt
manuell in Arrays umwandeln. Wenn wir konsequent auf reduce
gesetzt haben, müssen wir das nur an genau einem Punkt tun: vor der einen
for
-Schleife in reduce
in Zeile 2:
function reduce(array, start, combine) { if (array instanceof NodeList) { // konvertiere zu tatsächlichem Array array = Array.prototype.slice.call(array); } for (var i = 0, j = array.length; i < j; i++) { start = combine(start, array[i]); } return start; }
Auch die Wartbarkeit des Codes erhöht sich. Als Argument zu
reduce
wird eine Funktion übergeben, die exakt dokumentiert,
was mit einem Element geschehen soll. Es gibt kein Herumsuchen in
verschachtelten for
-Schleifen und die Funktionalität kann
leicht wiederverwendet werden.
Arrays zu Arrays: map
Eine weitere häufige Aufgabe ist, eine Funktion auf jedes Element eines
Array (oder Knotensets) anzuwenden. Jede unserer Boxen soll den initialen
Klickhandler zugewiesen bekommen. Wir verwenden dafür die Funktion
forEach
:
function forEach(array, func) { for (var i = 0, j = array.length; i < j; i++) { func(array[i]); } } forEach(alleBoxen, function(element) { var oeffner = element.parentNode .getElementsByClassName('.öffner')[0]; oeffner.addEventListener('click', erzeugeBox(element), false); });
Natürlich hätten wir hier die for
-Schleife direkt schreiben
können, aber ihre Abstraktion hinter forEach
bietet die
gleichen Vorteile wie bei reduce
. Benötigt man das Ergebnis des
Aufrufs von func
, z. B. alle Öffner-Buttons zu unseren
Klappboxen, nennt man die Funktion üblicherweise map
:
function map(array, func) { var newArray = []; for (var i = 0, j = array.length; i < j; i++) { newArray[i] = func(array[i]); } return newArray; } var alleOeffner = map(alleBoxen, function(element) { return element.parentNode.getElementsByClassName('.öffner')[0]; });
Mit IE 9 unterstützen mittlerweile alle Browser forEach
,
map
und reduce
als native Methoden von Arrays, die
in ECMAScript 5 spezifiziert wurden. Das obige Beispiel sieht dann so
aus:
alleBoxen.forEach(function(element) { var oeffner = element.parentNode .getElementsByClassName('.öffner')[0]; oeffner.addEventListener('click', erzeugeBox(element), false); });
Dass wir die Funktionen so benannt haben, ist natürlich kein Zufall. Sie sind in vielen funktionalen Sprachen bereits von Haus aus implementiert. Das eingangs erwähnte Google-Framework MapReduce leitet seinen Namen genau von ihnen ab. Warum wird ein Framework, dessen Aufgabe es ist, Billionen Rechenaufgaben auf tausende Computer zu verteilen, nach vielleicht 15 Zeilen Code benannt? Weil die Abstraktion, die durch sie eingeführt wird, es erlaubt, hinter der Bühne eine beliebige Implementierung umzusetzen.
Ein kleines JavaScript-Gedankenspiel dazu ist das Rendern komplexer
Strukturen in canvas
mit Web Workern.
Nehmen wir an, jeder Pixel eines Canvas muss für ein Spiel aufwändig
berechnet werden, hängt aber nicht von anderen Pixeln ab. Dann können wir
natürlich mit einer for
-Schleife durch alle Pixel loopen und
sie nacheinander berechnen. Viel effizienter wäre es jedoch, das Berechnen
parallel von WebWorkern erledigen zu lassen. Da die Pixel nicht voneinander
abhängen, kann jeder einzelne aktualisiert werden, wenn der Worker fertig
ist, und muss nicht auf seinen Vorgänger warten. Die konkrete
Implementierung mit map
ist denkbar einfach:
map(allePixel, function(pixel) { var worker = new Worker('berechne_pixel.js'); worker.onmessage = function(event) { context.putImageData(event.data, pixel.x, pixel.y); }; });
Eingangs habe ich erwähnt, dass sich Funktionales Programmieren dadurch
auszeichnet, dass es mehr betont, was getan werden soll, und nicht unbedingt,
wie es zu tun ist. Diese Struktur sehen wir jetzt recht deutlich, wenn wir
map
und reduce
ansehen und uns an Closures und
anonyme Funktionen erinnern. Aktionen werden in eine Funktion gekapselt, so
dass wir sie nach Belieben wiederverwenden können. An einer beliebigen
Stelle im Code können wir die Funktion direkt angeben, ohne uns erneut mit
den Details ihrer Implementierung auseinanderzusetzen.
Was geht (noch) nicht?
Ich habe oben die in rein funktionalen Sprachen vorherrschende Endgültigkeit von Variablendefinitionen erwähnt. Das ist etwas, das in JavaScript nicht bzw. nur in Teilen zur Verfügung steht. Eine praktische Auswirkung ist, dass automatische Memoisation nicht ohne weiteres funktioniert. Einfache Memoisation, das Wort stammt vom Memo, beschreibt das Zwischenspeichern von Funktionsergebnissen:
var fooCache = {}; function foo(arg) { if (arg in fooCache) { // wir haben das Ergebnis bereits irgendwann berechnet: return fooCache[arg]; } else { // berechne Ergebnis result = ... // und speichere: fooCache[arg] = result; return result; } }
Wären JavaScript-Funktionen nebenwirkungsfrei, könnte diese Best Practice vom JavaScript-Interpreter automatisiert werden und müsste nicht umständlich von Hand implementiert werden. Der Interpreter könnte sich bei jedem Funktionsaufruf einfach die Argumente merken und zukünftige Stellen im Code, die die gleiche Funktion mit den gleichen Parametern aufrufen, direkt durch das Ergebnis ersetzen, statt die Funktion noch einmal durchrechnen zu lassen. Man spricht in diesem Fall auch von referenzieller Transparenz, weil der umgebende Code nicht unterscheiden kann (oder muss), ob die Funktion tatsächlich aufgerufen oder durch ihren Rückgabewert ersetzt wird.
Ein Pattern, das man oft in funktionalen Programmen antrifft, ist der rekursive Funktionsaufruf. Er ist in solchen Sprachen nicht mit besonders hohen Kosten verbunden, wenn man eine gewisse Form einhält: Die sogenannte Endrekursion setzt immer dann ein, wenn der rekursive Aufruf das letzte Statement der entsprechenden Funktion ist. Sie ist ein Kniff, der den Speicherverbrauch von Rekursionen dadurch minimiert, dass der belegte Speicher der vorangegangenen Funktionsaufrufe dann wieder freigegeben wird. So werden Speicherüberläufe, wie sie in anderen Sprachen vorkommen können, effektiv vermieden. Endrekursion ist in JavaScript-Engines nicht implementiert, aber für zukünftige Versionen geplant.
Wie die erwähnte Klasse Car
in OOP-Tutorials wird für die FP
oft eine Funktion zur Berechnung der Fibonacci-Reihe als
Demonstrationsobjekt gewählt. Sie zeigt rekursiven Funktionsaufruf besonders
einfach.
function fibonacci(n) { if (n < 2) { return n; } else { return fibonacci(n - 1) + fibonacci(n - 2); } }
Die Rekursion in Zeile 5 ersetzt hier eine while
-Schleife,
die einen Zustand in Form einer temporären Variable verwenden müsste, um
ihre aktuelle Position zu speichern. Dieser temporären Variable entkommen
wir durch die Rekursion. Wir haben die Reihe vollständig zustandslos
umgesetzt. Dafür müssen wir berücksichtigen, dass Rekursion in JavaScript
nicht in beliebige Tiefen möglich ist, sondern vor einem drohenden
Speicherüberlauf vom Interpreter abgewürgt wird. Mit Endrekursion passiert
das nicht, sofern die Laufzeit im Rahmen bleibt.
Unendliche Reihen, z. B. die Reihe aller Fibonacci-Zahlen, sind
im Zusammenhang mit Rekursionen sehr nützlich. Sie geben in letzterem Fall
einen ausgezeichneten Datenpool ab, aus dem man sich in jeder
Rekursionsrunde bedienen kann, ohne dass er „austrocknet“. JavaScript
unterstützt etwas derartiges nicht von Haus aus, wie es z. B. in Haskell der
Fall ist, aber es wäre nicht JavaScript, wenn unendliche Reihen nicht von
einem findigen Entwickler umgesetzt worden
wären. Das Stichwort hierzu heißt Lazy
Evaluation. Lazy Evaluation ist auch das dynamische Nachladen von
Inhalten per Ajax. Wenn man an eine <ul>
-Liste denkt, die
schrittweise erweitert wird, findet man schnell nützliche Zusammenhänge mit
rekursiven Funktionen und auch unendlichen Reihen. Ein Beispiel ist, nur
ungerade Elemente der Liste zu laden und dafür die unendliche Reihe aller
ungeraden Zahlen Schritt für Schritt abzuarbeiten.
In dieses Horn stößt auch der Vorschlag, in ECMAScript
Generatoren einzuführen. Diese Sprachkonstrukte stellen eine
einfache Methode dar, komplizierte Reihen nur nach Bedarf durchzurechnen.
Bleiben wir beim Beispiel der Fibonacci-Zahlen. Wenn wir die Reihe aller
Fibonacci-Zahlen wollen, ist es nicht sinnvoll, einfach einen Array
anzufangen und zu füllen. Eine Funktion, die die nächste Zahl nur bei Bedarf
ausrechnet, ist viel nützlicher. JavaScript 1.7, wie es in Firefox seit
Version 2 implementiert ist, bietet dafür das Schlüsselwort
yield
an:
function Fibonaccis(){ var fn1 = 1, fn2 = 1, current; while (1) { current = fn2; fn2 = fn1; fn1 = fn1 + current; yield current; // hier wird gestoppt, bis der umgebende // Code den Iterator das nächste Mal // aufruft } } var Sequenz = Fibonaccis(); for (var zahl in Sequenz) { // gib nach und nach alle Fibonacci-Zahlen aus. Wenn wir // fertig sind, können wir die Schleife mit break verlassen }
Unglücklicher Weise ist das yield
-Schlüsselwort nicht für andere Browser
nachrüstbar, z. B. mit einem Polyfill. Man kann sich jedoch mit diversen Lösungen
behelfen, die zwar nicht so elegant in einer for
-Schleife
ausgelesen werden können, aber ihren Zweck ebenso erfüllen.
Viele weitere Kernbestandteile anderer funktionaler Sprachen sind nicht
in JavaScript enthalten, lassen sich aber im Nachhinein implementieren.
Beispiele dafür sind underscore.js, SugarJS und Functional
JavaScript, die genau damit werben. Auch große Bibliotheken wie jQuery
bieten das eine oder andere funktionale Schmuckstück an, z. B. jQuery.map()
.
// map in underscore.js... ergebnis = _.map(alleBoxen, function(element) { oeffner.addEventListener('click', erzeugeBox(element), false); }); // ...in Sugar.js... ergebnis = alleBoxen.map(function(element) { oeffner.addEventListener('click', erzeugeBox(element), false); }); // ...in Functional JavaScript... ergebnis = Functional.map(function(element) { oeffner.addEventListener('click', erzeugeBox(element), false); }, alleBoxen); // ...und in jQuery ergebnis = $.map(alleBoxen, function(element) { oeffner.addEventListener('click', erzeugeBox(element), false); });
Funktionale Programmierung ist ein Werkzeug
Unter JavaScript-Programmierern mit OOP-Hintergrund erfreut sich backbone.js als Quasi-MVC-Framework steigender Beliebtheit. Es ist nicht ohne ein bisschen Ironie, dass dessen einzige harte Abhängigkeit das gerade erwähnte underscore.js ist, eine Bibliothek, die fehlende Elemente anderer funktionaler Sprachen in JavaScript umsetzt.
Andererseits zeigt diese Symbiose sehr schön die Flexibilität von JavaScript und die Vielseitigkeit und Mächtigkeit, die sich aus dem Mischen verschiedener Paradigmen ergibt. Wenn man einmal angefangen hat, funktionale Features zu verwenden, wird man Sprachen verfluchen, die Funktionen nicht als Objekte erster Klasse behandeln. Es wird sich anfühlen wie ein Werkzeugkasten, aus dem jemand alle Schraubenzieher gestohlen hat.