Über unsMediaKontaktImpressum
Marco Emrich 14. November 2017

Higher Order-Funktionen mit modernem JavaScript

Higher Order-Functions sind gar nicht neu in JavaScript, sondern waren von Anfang an verfügbar. © Givaga / Fotolia.com
© Givaga / Fotolia.com

Der Sprachstandard ECMAScript 2015 (kurz: ES6) bedeutete für JavaScript eine völlige Runderneuerung. Insbesondere funktionale Sprachkonzepte sind plötzlich salonfähig geworden. Obwohl sich ES6 nach über zwei Jahren in der Breite durchgesetzt hat, nutzen viele Entwickler oft nur einen kleinen Teil der Neuerungen. Dieser Artikel wirft einen Blick auf die sogenannten "Higher Order-Functions" und zeigt, wie Sie damit in ES6 deutlich lesbareren und wartbareren Code entwickeln können.

Höhere Ordnung von Anfang an?

Zunächst einmal sind Higher Order-Functions gar nicht neu in JavaScript, sondern waren von Anfang an verfügbar – dazu gleich mehr.

Von seinen funktionalen Vorbildern (z.  B. der Programmiersprache Scheme) hat JavaScript die Besonderheit geerbt, dass Funktionen einfach Werte sind. Genauso, wie Sie eine Zahl einer Variable zuordnen können, können Sie das auch mit Funktionen tun.

var price = 3;
var add = function(a, b) {return a + b};

In ES6-Syntax sieht das dann so aus:

let add = (a, b) => a + b;

Statt dem Schlüsselwort function benötigen Sie lediglich den sogenannten "fat arrow" =>. Das andere Schlüsselwort return kann bei Einzeilern komplett entfallen. Als moderner Ersatz für var fungieren hier übrigens let und const. Der Unterschied ist für diesen Artikel aber nicht weiter wichtig.

Wenn Sie nach dem Typ der Variablen fragen, erhalten Sie die entsprechende Antwort:

typeof price; // => number
typeof add;   // => function

Die "funktionale Programmierung" spricht von sogenannten "first class functions". Damit ist gemeint, dass die Sprache Funktionen genauso behandelt wie andere Werte. Sie können sie mit Variablen referenzieren und Sie können sie sogar als Argumente übergeben. Gerade letzteres lässt sich oft gut gebrauchen.

Die Hohe Kunst des Sortierens

Betrachten Sie dazu einmal die klassische sort-Methode von Arrays. Eine relativ häufige Anforderung besteht darin, numerische Arrays nach ihren Werten zu sortieren. Versuchen Sie doch mal folgendes:

[16, 10, 2, 12, 1].sort();

Wünschen würden Sie sich vermutlich:

[1, 2, 10, 12, 16]

Sie erhalten aber:

[1, 10, 12, 16, 2]

Was ist hier schiefgelaufen?

Die sort-Funktion führt standardmäßig eine sogenannte "lexikographische" Sortierung durch. Dabei behandelt sie Zahlen genauso wie Strings. Die Zahl 10 wird als kleiner als die 2 betrachtet, da 10 mit der Ziffer 1 beginnt. Erst wenn sort die erste Ziffer verglichen hat, nimmt sie sich die zweite vor. Deswegen ist 12 durchaus kleiner als 16, aber lexikographisch betrachtet auch 10 kleiner als 2.

Zum Glück lässt sich das Verhalten von sort beeinflussen. Sie können der Funktion mitteilen, welchen von zwei Werten sie als kleiner und welchen sie als größer erachten soll. Dazu benötigen Sie eine weitere Funktion, die zwei Parameter entgegennimmt und diese miteinander vergleicht. Dabei erwartet sort von der Vergleichsfunktion, dass sie:

  • "0" zurückgibt, falls a und b gleich sind
  • eine "positive Zahl" zurückgibt, falls a größer als b ist
  • eine "negative Zahl" zurückgibt, falls a kleiner als b ist

Sie erreichen das bei Zahlen z.  B. einfach dadurch, dass sie b von a subtrahieren.

Mit "altem" JavaScript ginge das so:

var compareNumerical = function(a, b) {
  return a - b;
}

[16, 10, 2, 12, 1].sort(compareNumerical); // => [1, 2, 10, 12, 16]

Hier das Gleiche in etwas kompakterer ES6-Syntax:

var compareNumerical = (a, b) => a - b;

[16, 10, 2, 12, 1].sort(compareNumerical); // => [1, 2, 10, 12, 16]

Prinzipiell können Sie die Funktion auch direkt definieren, ohne sie vorher einer Variable zuzuweisen.

[16, 10, 2, 12, 1].sort( (a, b) => a - b ); // => [1, 2, 10, 12, 16]

In der alten Syntax wäre das selbst bei diesem einfachen Beispiel schwer zu lesen:

[16, 10, 2, 12, 1].sort( function(a, b) {return a - b} ); // => [1, 2, 10, 12, 16]

Trotzdem ist der Code in vielen Fällen besser lesbar, wenn Sie die Funktion vorher einer Variablen zuweisen. Durch den zusätzlichen Variablennamen compareNumerical kann ein Wartungsprogrammierer die ursprüngliche Absicht (numerischer Vergleich) leichter nachvollziehen. Aber es gibt auch Fälle, wo es sich kaum lohnt, die Funktionen in Variablen auszulagern.

IT-Tage 2017 - Softwareentwicklung

Funktionen, die auf Funktionen stehen

Funktionen wie sort, deren Parameter selbst wieder Funktionen sind oder die Funktionen als Rückgabewert zurückgeben, heißen Higher Order-Functions – Funktionen höherer Ordnung. sort gab es bereits in der ersten Version von ECMAScript [1]. Andere Funktionen wie z. B. map oder reduce kamen erst mit ES5 dazu [2], und die moderne ES6-Syntax (z. B. Fat Arrow) schließlich sorgt für praktikable Verwendung.

Arrays im Wandel mit map

Eine weitere sehr typische Problemstellung besteht darin, dass Sie alle Elemente eines Arrays in irgendeiner Form weiterverarbeiten möchten. Stellen Sie sich beispielsweise vor, sie haben ein Liste von Anwendern als Objekte mit Vor- und Nachnamen vorliegen und möchten diese in ein spezielles Ausgabeformat überführen.

var users = [
  {name: 'Reich', firstName: 'Frank'},
  {name: 'Huana', firstName: 'Marie'},
  {name: 'Meisenbär', firstName: 'Andreas'}
];

Das Ausgabeformat sieht vor, den Vornamen abzukürzen. Außerdem müssen es Strings sein, damit sie sich direkt auf der Website ausgegeben lassen. So möchten Sie z. B. das Objekt {name: 'Reich', firstName: 'Frank'} in den String "F. Reich" konvertieren. Eine klassische Lösung könnte so aussehen:

function formattedUserNames(users) {
  var userNamesFormatted = [];

  for (var i = 0; i < users.length; ++i) {
    userNamesFormatted.push(users[i].firstName[0] + '. ' + users[i].name);
  }
  return userNamesFormatted;
}

Viele Teile dieser Implementierung fallen in den Bereich der "accidental complexity" – also Komplexität, die nicht zur Lösung des Problems beträgt. Dazu gehört beispielsweise die Schleife mit ihrem Index oder die Hilfsvariable userNamesFormatted.

Sie benötigen diese Konstrukte, um das "Wie" der Lösung zur realisieren – reine Mechanik, die nichts über Ihre eigentliche Absicht, Ihre Intention, das "Was" aussagt. Ihr eigentliche Intention ist es, eine Liste von Werten in eine andere Liste zu übertragen, indem Sie jedes einzelne Element mit einer bestimmten Vorschrift übertragen. Genau hier tritt die Higher Order-Function map auf den Plan.

function formattedUserNames(users) {
  return  users.map(
    user => user.firstName[0] + '. ' + user.name
  );
}

Die Funktion map bekommt eine andere Funktion als Argument übergeben. Das ist hier die Funktion user => user.firstName[0] + '. ' + user.name. Diese Funktion dient als Transformationsvorschrift.

Da sie keinen Namen hat, redet man meist von einer "anonymen Funktion". Sie können auch "callback" dazu sagen, weil map die Funktion zurückruft. Der passende Begriff aus der funktionalen Programmierung ist "lambda".

Zunächst fällt auf, das die neue Lösung kürzer ist. Der eigentliche Gewinn liegt aber nicht in der geringeren Menge an Codezeilen, sondern an der klareren Intention. Die Mechanik der Transformation (das "Wie") ist verschwunden. Es gibt keine Schleife, keinen Index und keine Hilfsvariable mehr. Stattdessen erkennen Sie das "Was" auf einen Blick. Der Aufruf von map zeigt Ihnen, dass ein neues Array mit der gleichen Anzahl von Elementen entsteht. Was mit jedem einzelnen Element geschieht, zeigt die anonyme Funktion.

Mit Hilfe von ES6 lässt sich der Code noch weiter optimieren.

const formattedUserNames = users =>
  users.map(
    user => `${user.firstName[0]}. ${user.name}`
  );

Ob man das tatsächlich lesbarer findet, ist sicherlich ein wenig Geschmacksache. Meiner Erfahrung nach ist es lesbarer, sobald man sich erstmal an die neue Syntax gewöhnt hat.

Nur sehen, was man sehen will, mit filter

Betrachten Sie für die nächste Higher Order-Funktion mal die folgende Anforderung eines (gedachten) Auftraggebers:

Wir haben festgestellt, dass unsere Kunden gerne Produktlisten ausdrucken, um sie ihren Freunden zu zeigen. Damit diese Listen etwas besser strukturiert sind, möchten wir sie wie ein Stichwortverzeichnis in einem Buch aufbauen. Das heißt, zuerst wird der Buchstabe als Überschrift gelistet und darunter alle Produkte, die mit diesem Buchstaben beginnen. Können Sie das umsetzen?

Natürlich können Sie! Schreiben Sie zunächst eine Funktion, die einen Buchstaben und die Produkte als Argumente bekommt. Zurückgeben muss sie alle Produkte, die mit diesem Buchstaben beginnen. Folgendes Funktionsgerüst könnte helfen:

const productsStartingWith = (letter, products) => ...

Jetzt hilft Ihnen die Funktion "filter" weiter. Genau wie map können Sie filter auf Arrays aufrufen, und filter betrachtet jedes Element des Arrays einzeln. Ebenfalls genau wie bei map können Sie filter eine Funktion als Argument übergeben, die filter für jedes Element einzeln ausführt. An der Stelle enden aber die Gemeinsamkeiten. filter erwartet eine Funktion mit booleschem Rückgabewert — also eine "Bedingung".

const productsStartingWith =
  (letter, products) => products.filter(
    product => ...
  );

Stellt sich nur noch die Frage: Wie lautet die Bedingung? Da Sie alle Elemente (d. h. Produkte) suchen, die mit einem bestimmten Buchstaben beginnen, können Sie schreiben:

product.startsWith(letter)

Hier nochmal in zusammengebauter Form:

const productsStartingWith =
  (letter, products) => products.filter(
    product => product.startsWith(letter)
  );

Probieren Sie die neue Funktion aus!

let products = ["Marvel Comics Lightweight Infinity Scarf", "Ollie - The App Controlled Robot", "Meh Hoodie", "Magnetic Accelerator Cannon", "Aquafarm: Aquaponics Fish Garden"];

console.log(productsStartingWith("M", products));
// => ["Marvel Comics Lightweight Infinity Scarf", "Meh Hoodie", "Magnetic Accelerator Cannon"]

Der Nutzen von filter dürfte klar sein. Das Beispiel lässt sich nun leicht vervollständigen. Um die gesamte Produktliste zu erhalten, müssen Sie das Spielchen für alle Buchstaben wiederholen – oder zumindest für alle relevanten. Dazu brauchen Sie nochmal die map-Funktion.

"AMO".split("").map(
  letter =>
    `\n==== ${letter} ====
     \n${productsStartingWith(letter, products).join("\n")}
    `).join("\n");

Nach dem Ausführen erhalten Sie folgende Ausgabe:

==== A ====
Aquafarm: Aquaponics Fish Garden

==== M ====
Marvel Comics Lightweight Infinity Scarf
Meh Hoodie
Magnetic Accelerator Cannon

==== O ====
Ollie - The App Controlled Robot

Aufs Wesentliche reduzieren mit reduce


Eine weitere Anforderung unseres gedachten Kunden könnte sein:

Bevor wir das neue Bestellsystem aktivieren können, muss der Warenkorb noch den Gesamtpreis der Produkte im Warenkorb – der sogenannten Cart Items – anzeigen.

Das ist nicht weiter schwierig – die Produktpreise liegen bereits als Array vor.

let cartItemPrices = [9.99, 19.99, 5.99];

Um die Preise aufzusummieren, benötigen Sie zwei Dinge:

  • Die Higher Order-Funktion reduce

  • die Addition als Funktion (statt nur den Operator)

Die reduce-Funktion reduziert ein Array auf einen einzelnen Wert. Dazu benötigt Sie eine Funktion als Argument, die angibt, wie aus jeweils zwei Werten einer wird.

Definieren Sie dafür zunächst eine eigene Funktion add:

const add = (a, b) => a + b; 

Diese Funktion beschreibt einfach die Addition von zwei Zahlen. Wenn Sie die Funktion auf alle Elemente im Array anwenden, erhalten Sie die Gesamtsumme aller Einzelwerte über das Array. 
Übergeben Sie nun die Funktion add als Argument an reduce.

const add = (a, b) => a + b;


let cartItemPrices = [9.99, 19.99, 5.99];
let sum = cartItemPrices.reduce(add);

console.log(sum); // => 35.97

Das ist wirklich schon alles. Sie können reduce eine beliebige Funktion übergeben, solange diese zwei Werte zu einem zusammenschrumpft.

Schauen Sie sich mal an, wie reduce hinter den Kulissen arbeitet. Die Higher Order-Funktion nimmt sich den ersten Wert aus dem Array (9.99) und setzt ihn als ersten Parameter in add ein. Als zweiten Parameter verwendet sie das zweite Element aus dem Array (19.99). reduce ruft dann die übergebene Funktion add auf und merkt sich das Ergebnis.

add(9.99, 19.99) // => 29.98 

Nun erfolgt ein weiterer Aufruf von add. Das gemerkte Ergebnis des letzten Aufrufs ist nun das neue erste Argument. Für den zweiten Parameter von add nimmt sich reduce das dritte Element aus dem Array (5.99).

add(29.98, 5.99) // => 35.97

Damit ist die Berechnung abgeschlossen und reduce gibt 35.97 zurück. Gäbe es weitere Elemente im Array, würde die Funktion genauso weiter verfahren:

add(29.98, /* 4.Element */) // => Sum 1 to 4
add(/*Sum 1 to 4 */, /* 5.Element*/) // => Sum 1 to 5
add(/*Sum 1 to 5 */, /* 6.Element*/) // => Sum 1 to 6
// etc.

Als kleine Verbesserung können Sie die Summe als eigenständige Funktion auslagern:

const add = (a, b) => a + b;

const sum = values => values.reduce(add);

let cartItemPrices = [9.99, 19.99, 5.99];

console.log(sum(cartItemPrices)); // => 35.97

Weitere Higher Order-Functions

Das war natürlich noch längst nicht alles. So gibt es gerade auf dem Array-Prototype noch viele weitere Higher Order-Functions, wie z. B. diese vier häufig verwendeten:

  • some: Gibt true zurück, falls einige Elemente der angegebenen Bedingung (callback) entsprechen.
  • every
: Gibt true zurück, falls alle Elemente der angegebenen Bedingung (callback) entsprechen.
  • find: Findet das erste Element, das die angegebene Bedingung (callback) erfüllt.
  • findIndex: Gibt den Index des ersten Elements zurück, das die angegebene Bedingung (callback) erfüllt.

Zwei Schritte voraus mit funktionalen Bibliotheken

Wem das, was JavaScript im Sprachumfang zu bieten hat, noch nicht reicht, der findet viele weitere Higher Order-Funktionen und sogar weitere funktionale Konzepte in externen Bibliotheken – hier eine Auswahl:

  • Ramda: Ramda ist eine Bibliothek, die JavaScript um zusätzliche funktionale Konzepte (currying, transducer, etc.) erweitert. Darunter sind auch viele zusätzliche Higher Order-Functions. Ramda ist neben einem Testing-Framework meist das erste, was ich in einem neuen JavaScript-Projekt installiere [3].
  • Lodash: Lodash ist mit Ramda vergleichbar, aber schon ein wenig älter. Mit dem neueren Zweig lodash/fp hat es nachgelegt und stellt nun durchaus eine gute Alternative zu Ramda dar [4].
  • Immutable: Immutable von Facebook stellt Datenstrukturen wie Map oder Set in einer unveränderlichen (immutable) Fassung zur Verfügung. Sie bringen jeweils eigene Implementierungen gängiger Higher Order-Functions wie map oder reduce mit [5].

Fazit

Auch wenn Higher Order-Funktionen nicht wirklich neu sind, so haben sie doch mit den neueren JavaScript-Sprachversionen erheblichen Aufwind erfahren. Sie stellen oft eine bessere Alternative zu klassischen Schleifen dar und belohnen mit besserer Lesbarkeit und kompakterem Code.

Moderne JavaScript-Versionen lassen sich glücklicherweise auch in der Praxis (nahezu) uneingeschränkt verwenden. Dank Transpilern wie Babel sind sie selbst auf älteren Browsern benutzbar [6]. Fangen Sie also am Besten gleich damit an, von den Vorteilen von Higher Order-Funktionen in Verbindung mit ES6 zu profitieren. Wenn Sie noch weiter gehen möchten, werfen Sie einen Blick in eine moderne Sprachbibliothek für Funktionale Programmierung. Es lohnt sich!

Quellen
  1. ECMAScript: MDN web docs: Array.prototype.sort()
  2. ES5: Juriy "kangax" Zaytsev: ECMAScript5 Compatibility Table
  3. Ramda
  4. Lodash
  5. Facebook: Immutable
  6. ES6/ Babel: Juriy "kangax" Zaytsev: ECMAScript6 Compatibility Table
nach Oben
Autor

Marco Emrich

Marco Emrich ist leidenschaftlicher Verfechter der Software-Craft-Bewegung. Er verfügt über langjährige Erfahrung als Software-Architekt und -Entwickler in unterschiedlichen Branchen.
>> Weiterlesen
Buch des Autors:

botMessage_toctoc_comments_929