Konsole & …: Werkzeugkasten

the next big thing  –  3 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.

Die vergangene Folge hat die Funktionen map zur Projektion und reduce zur Aggregation vorgestellt und deren Beziehung zueinander thematisiert. Doch die funktionale Programmierung kennt weitaus mehr als nur diese beiden Funktionen, auch wenn sie in der Regel im Vordergrund stehen.

forEach

Verwandt mit der map- ist die forEach-Funktion: Wie map führt auch sie eine ihr als Parameter übergebene Funktion für jedes Element eines Arrays aus, gibt allerdings keine Werte zurück. In gewissem Sinne entspricht die forEach- daher einer map-Funktion, deren Rückgabewert verworfen wird. In JavaScript lässt sie sich auf einfachem Weg implementieren:

var forEach = function (values, fn) {
for (var i = 0; i < values.length; i++) {
fn(values[i]);
}
};

filter

Die filter-Funktion wird häufig in Verbindung mit der map-Funktion verwendet: Vergleicht man JavaScript mit SQL, verhält sich map zu filter wie SELECT zu WHERE. Wie auch map gibt filter ein Array von Werten zurück, allerdings kann dieses – je nach Filterkriterium – weniger Elemente enthalten als das zu filternde Array. Auch diese Funktion lässt sich in JavaScript auf einfachem Weg implementieren:

var filter = function (values, fn) {
var result = [];
for (var i = 0; i < values.length; i++) {
if (fn(values[i]) {
result.push(values[i]);
}
}
return result;
};

Die Funktion, die das Filterkriterium implementiert, wird in der funktionalen Programmierung in der Regel als Prädikatfunktion bezeichnet.

every

Die every-Funktion unterscheidet sich von map und filter primär dadurch, dass sie kein Array von Werten zurückgibt, sondern nur einen einzigen logischen Wert. Sie prüft, ob alle ihr übergebenen Werte eine ebenfalls übergebene Prädikatfunktion erfüllen oder nicht: Im Erfolgsfall gibt sie true zurück, andernfalls false. Ihre Implementierung fällt in JavaScript ebenfalls leicht:

var every = function (values, fn) {
for (var i = 0; i < values.length; i++) {
if (!fn(values[i])) {
return false;
}
}
return true;
};

some

Die some-Funktion entspricht einer schwächeren Form von every: Sie gibt bereits dann den Wert true zurück, wenn mindestens ein Element die Prädikatfunktion erfüllt. Auch ihre Implementierung lässt sich in JavaScript leicht durchführen:

var some = function (values, fn) {
for (var i = 0; i < values.length; i++) {
if (fn(values[i])) {
return true;
}
}
return false;
};

Lisp und LINQ

Prinzipiell sind die Ideen hinter diesen Funktionen alles andere als neu: Beispielsweise gehen die Begriffe map und reduce auf Lisp zurück. Allerdings werden diese Ideen nicht von jeder Sprache gleichermaßen wertgeschätzt, weshalb es nach wie vor Sprachen gibt, die diese Funktionen nicht nativ unterstützen.

Sehr lesenswert in diesem Zusammenhang ist übrigens der Blogeintrag "Can Your Programming Language Do This?" von Joel Spolsky, in dem er einen Zusammenhang zwischen funktionaler Programmierung auf der einen und Google und Microsoft auf der anderen Seite herstellt:

"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. MapReduce is, in retrospect, obvious to anyone who remembers from their 6.001-equivalent programming class that purely functional programs have no side effects and are thus trivially parallelizable. The very fact that Google invented MapReduce, and Microsoft didn't, says something about why Microsoft is still playing catch up trying to get basic search features to work, while Google has moved on to the next problem: […]"

Apropos Microsoft: Auch die in C# 3.0 eingeführte, in die Programmiersprache eingebettete Abfragesprache LINQ ist prinzipiell nichts anderes als eine Implementierung dieser funktionalen Konzepte.

Komplexe Abfragen

Das Praktische an all diesen Funktionen ist, dass sie sich frei miteinander kombinieren lassen, sei es per Hand oder per Funktionskomposition. Auf diesem Weg kann man beispielsweise aus einer Liste von natürlichen Zahlen lediglich die geraden Zahlen ermitteln, diese quadrieren und anschließend ausgeben:

[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
.filter(function (n) { return n % 2 === 0; })
.map(function(n) { return n * n; })
.forEach(function (n) { console.log(n); });

// => 4, 16, 36, 64, 100

Es liegt auf der Hand, dass sich das gleiche Vorgehen auf beliebige andere Abfragen anwenden lässt. Vergleicht man diesen Code nun mit einer klassischen, prozeduralen Implementierung, fällt zunächst die andere Schreibweise auf:

var numbers = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
for (var i = 0; i < numbers.length; i++) {
var n = numbers[i];
if (n % 2 === 0) {
console.log(n * n);
}
}

Auf den zweiten Blick ist dieser Code hinsichtlich der Laufzeit sogar überlegen, führt er die Schleife doch nur ein einziges Mal aus! Die funktionale Variante hingegen führt drei separate Schleifen aus – für filter, map und forEach. Wo also liegt der Vorteil der funktionalen Programmierung? Die Antwort auf diese Frage wird die nächste Folge von "Konsole & Kontext" geben …

tl;dr: Die Funktionen map und reduce sind nicht die einzigen generischen Funktionen, die in der funktionalen Programmierung Verwendung finden: Zu ihnen gesellen sich beispielsweise forEach, filter, every und some. Auf diesem Weg lassen sich komplexe Abfragen mit einfachen Sprachmitteln schreiben, deren Leistung sich allerdings noch optimieren lässt.