Konsole & …: Funktionen höherer Ordnung

the next big thing  –  0 Kommentare

"Konsole & Kontext" ist eine gemeinsame Serie von Golo Roden und Michael Wiedeking, in der sich die beiden regelmäßig mit Konzepten der (funktionalen) Programmierung beschäftigen. Während "Konsole & …" die praktische Anwendung dieser Konzepte in JavaScript und Node.js zeigt, erläutert "... & Kontext" deren jeweiligen eher theoretischen Hintergrund.

Wie in der vergangenen Folge beschrieben, sind Lambda-Ausdrücke in JavaScript Bürger erster Klasse. Das bedeutet, dass man Funktionen auf die gleiche Art wie Daten an andere Funktionen übergeben und sie als Rückgabewert verwenden kann.

Funktionen, die andere Funktionen als Parameter entgegennehmen oder eine Funktion als Wert an ihren Aufrufer zurückgeben, bezeichnet man in der funktionalen Programmierung als "Funktionen höherer Ordnung". Sie sind in vielerlei Hinsicht ausgesprochen hilfreich und unter anderem für die Verwendung der in JavaScript und Node.js üblichen Rückruffunktionen zwingend erforderlich.

Addieren und multiplizieren

Ein gutes Beispiel für den Einsatz einer Funktion höherer Ordnung liefert die Aufgabe, die Summe aller in einem Array enthaltenen Zahlen zu berechnen. Der imperative Ansatz sieht eine Schleife vor, in der man die einzelnen Elemente nach und nach zu einer gemeinsamen Summe addiert:

var add = function (numbers) {
var sum = 0;
for (var i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
};

Vergleicht man das mit der Funktion zum Berechnen des Produkts aller in einem Array enthaltenen Zahlen, dann fallen zahlreiche Gemeinsamkeiten auf:

var multiply = function (numbers) {
var product = 1;
for (var i = 0; i < numbers.length; i++) {
product *= numbers[i];
}
return product;
};

Abgesehen von den verwendeten Bezeichnern, dem initialen Wert des Zwischenergebnisses und der eigentlichen Rechenoperation gleichen sich beide Funktionen vollständig. Es liegt daher nahe, die identischen Bestandteile in eine neue, gemeinsam verwendete Funktion auszulagern und dieser lediglich die Unterschiede als Parameter zu übergeben:

var calculate = function (numbers, init, combine) {
var result = init;
for (var i = 0; i < numbers.length; i++) {
result = combine(result, numbers[i]);
}
return result;
};

Nun kann man sowohl die Summe als auch das Produkt auf einfachem Weg berechnen. Es gilt lediglich, einen geeigneten Initialwert und die gewünschte Operation zu übergeben:

var numbers = [ 2, 3, 5, 7, 11 ],
add = function (a, b) { return a + b; },
multiply = function (a, b) { return a * b; };

var sum = calculate(numbers, 0, add);
var product = calculate(numbers, 1, multiply);

console.log(sum); // => 28
console.log(product); // => 2310

Die reduce-Funktion

Die calculate-Funktion abstrahiert allerdings nicht nur die Operation, sondern auch die verwendeten Typen: Im Gegensatz zu den ursprünglichen Funktionen ist die Funktion höherer Ordnung nicht mehr auf Zahlen festgelegt, sondern kann beispielsweise auch aus einem Array von Zeichenketten den alphabetisch kleinsten Wert ermitteln:

var animals = [ 'Pinguin', 'Eisbär', 'Tiger' ],
isLessThan = function (a, b) { return a < b ? a : b; };

var animal = calculate(animals, 'Zebra', isLessThan);

console.log(animal); // => 'Eisbär'

Prinzipiell funktioniert dieses Beispiel zwar, allerdings wirkt der Funktionsname calculate reichlich ungeeignet: Er legt das Berechnen von Zahlen nahe, stattdessen vergleicht man Zeichenketten.

Trotzdem gibt es einen gemeinsamen Nenner: In allen Beispielen reduziert man eine Liste von Werten auf einen einzelnen Wert. Daher trägt diese Funktion in der funktionalen Programmierung den Namen reduce.

Seit EcmaScript 5 enthält JavaScript eine vorgefertigte reduce-Funktion, die man direkt auf einem Array aufrufen kann. Das zuvor genannte Beispiel kann man also nativ wie folgt formulieren:

var animals = [ 'Pinguin', 'Eisbär', 'Tiger' ],
isLessThan = function (a, b) { return a < b ? a : b; };

var animal = animals.reduce(isLessThan, 'Zebra');

console.log(animal); // => 'Eisbär'

Die Angabe des Initialwerts ist bei der in JavaScript serienmäßig enthaltenen reduce-Funktion optional: Verzichtet man auf diesen, verwendet JavaScript für den ersten Aufruf der Operation die ersten beiden Einträge des angegebenen Arrays.

Vorteile von Funktionen höherer Ordnung

Grundsätzlich kann man über die Frage, ob ein höherer Abstraktionsgrad von Code wünschenswert ist oder nicht, natürlich streiten. Dennoch weist die reduce-Funktion gegenüber dem imperativen Ansatz unbestreitbar zwei Vorteile auf.

Erstens trennt dieses Vorgehen verschiedene Belange in unterschiedliche Codeblöcke: Während man im imperativen Fall das Iterieren mit dem Berechnen vermischt, trennt die funktionale Lösung beide Belange sauber voneinander. Der Vorteil hierbei besteht darin, dass Leistungsverbesserungen in der reduce-Funktion automatisch allen auf ihr aufbauenden Berechnungen zu Gute kommen, ohne dass man diese hierfür von Hand anpassen müsste.

Zweitens vermeidet dieses Vorgehen redundanten Code, da man gemeinsame Bestandteile in eine einzige Funktion auslagern kann. Auch dies ist zweifelsohne hilfreich, da die Codemenge sinkt, was die Wartung und Weiterentwicklung vereinfacht. Zudem muss man potenziell auftretende Fehler nicht mehrfach beheben, was langfristig der Codequalität zu Gute kommt.

Beide Aspekte sind keine Besonderheit der reduce-Funktion, sondern symptomatisch für den Einsatz von Funktionen höherer Ordnung.

tl;dr: Funktionen höherer Ordnung sind Funktionen, die Funktionen wie Daten als Parameter und Rückgabewerte verwenden. Ein gutes Beispiel ist die in JavaScript serienmäßig enthaltene reduce-Funktion, mit der man die einzelnen Elemente eines Arrays auf einen einzigen Wert reduziert. Sie zeigt zugleich, wie man mit Funktionen höherer Ordnung einen höheren Abstraktionsgrad erreicht.