Funktionales Programmieren: das vernachlässigte Paradigma

Manuel Strehl
Programmieren ist zu einem wesentlichen Teil Abstrahieren von konkreten Zuständen und Abläufen. Was wäre, wenn neben den bekannten Objekten eine weitere kraftvolle Art der Abstraktion in JavaScript nur darauf wartete, eingesetzt zu werden? Das funktionale Programmier-Paradigma (FP), das schon seit langem von seinen Anhängern für seine Flexibilität geschätzt wird, ist ein Kernbestandteil von JavaScript. Während Objekte gut geeignet sind, Zustände darzustellen, bietet die FP Möglichkeiten, um Abläufe zu abstrahieren. Der Artikel gibt einen Überblick über ihre Elemente und wie sie zu besserem Code führen können.

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.

Foto des Autors Manuel Strehl verwendet JavaScript gerne für allerlei typografische Spielereien. Seine ersten funktionalen Schritte machte er einst in XSLT. Wenn er nicht gerade seine Freizeit mit Github-Projekten schmälert, seine Doktorarbeit vorantreibt oder als @m_strehl twittert, arbeitet er als Frontend-Entwickler bei Kinetiqa, einer Webagentur in Regensburg. Er bloggt auf manuel-strehl.de.