Ich kenne was, was Du nicht kennst: Highland

Die reaktive Programmierung ist ein Programmierparadigma, das auf Streams und deren Komposition basiert. Für JavaScript und Node.js existieren verschiedene Module, die das Vorgehen implementieren und mit den hauseigenen Mechanismen der Sprache integrieren. Von diesen Modulen macht vor allem Highland eine gute Figur.

In Pocket speichern vorlesen Druckansicht 2 Kommentare lesen
Lesezeit: 5 Min.
Von
  • Golo Roden
Inhaltsverzeichnis

Die reaktive Programmierung ist ein Programmierparadigma, das auf Streams und deren Komposition basiert. Für JavaScript und Node.js existieren verschiedene Module, die das Vorgehen implementieren und mit den hauseigenen Mechanismen der Sprache integrieren. Von diesen Modulen macht vor allem Highland eine gute Figur.

Streams ermöglichen das effiziente Verarbeiten großer Datenmengen, ohne dazu sämtliche Daten gleichzeitig im Speicher vorhalten zu müssen. Auf dem Weg lassen sich beispielsweise auch extrem große Dateien effizient komprimieren:

'use strict';

const fs = require('fs');
const zlib = require('zlib');

const source = fs.createReadStream('/in.txt');
const target = fs.createWriteStream('/out.txt');

const gzip = zlib.createGzip();

source.pipe(gzip).pipe(target);
Mehr Infos

"Ich kenne was, was Du nicht kennst"

... ist eine gemeinsame Serie von Golo Roden und Philip Ackermann, in der die beiden regelmäßig Module für JavaScript und Node.js vorstellen.

Der Umgang mit Streams gestaltet sich allerdings nicht immer so einfach. Abgesehen von einigen Unzulänglichkeiten der Streams-API von Node.js fällt das Verarbeiten eines Streams nur dann leicht, wenn es einen geeigneten Stream gibt, der die Transformation übernimmt. Solche Streams selbst zu schreiben ist zwar möglich, aber aufwendig und umständlich.

Außerdem funktionieren derartige Streams ausschließlich in Verbindung mit anderen Streams: Will man beispielsweise die Daten eines Arrays als Rohmaterial verwenden, muss man das Array zunächst in einen Stream konvertieren. Auch das ist aufwendig, obwohl Arrays und Streams einander prinzipiell sehr ähnlich sind, schließlich enthalten beide Daten. Der Unterschied zwischen Arrays und Streams liegt lediglich im Zeitpunkt, wann diese Daten verfügbar sind.

Einen Ausweg stellt die reaktive Programmierung dar, die das Verarbeiten von Streams und deren Komposition in den Fokus rückt. Für JavaScript beziehungsweise Node.js gibt es verschiedene Module zu diesem Zweck, unter anderem Highland, Bacon.js und RxJS. Welchem der Module man den Vorzug gibt, ist im Wesentlichen eine Frage des persönlichen Geschmacks und des gewählten Schwerpunkts.

Das Modul Highland stammt von Caolan McMahon, der vielen JavaScript- und Node.js-Entwicklern als Autor von Async.js bekannt sein dürfte. Daher fällt die Verwendung des Moduls ausgesprochen leicht, wenn man bereits über Erfahrung mit Async.js verfügt. Man sieht beiden Modulen eine sehr ähnliche Denkweise an.

Um Highland verwenden zu können, ist es zunächst in den lokalen Anwendungskontext zu installieren. Das gelingt auf dem üblichen Weg mit Hilfe von npm (Node Package Manager):

$ npm install highland

Danach ist das Modul noch mit der require-Funktion in die eigene Anwendung zu laden. Als Bezeichner schlägt die Dokumentation den Unterstrich vor. Bereits an der Stelle zeigt sich die gedankliche Nähe zu Async.js:

const _ = require('highland');

Highland enthält eine eigene Implementierung von Streams, weshalb es erforderlich ist, herkömmliche Node.js-Streams in Highland-Streams zu konvertieren. Das gelingt durch den Aufruf der _-Funktion, die außer Streams auch Arrays, Generatorfunktionen, Event-Emitter, Promises und Iteratoren entgegennehmen und umwandeln kann.

Der Rückweg erfolgt beispielsweise mit der Funktion toArray. Weitaus nützlicher ist allerdings die Möglichkeit, einen Highland-Stream wieder in einen klassischen Node.js-Stream umwandeln zu können, indem man auf die pipe-Funktion zurückgreift, die auch Highland unterstützt.

Zum Verarbeiten von Streams stehen in Highland zahlreiche Funktionen zur Verfügung, beispielsweise die aus der funktionalen Programmierung bekannte map-Funktion. Im Unterschied zu der in JavaScript integrierten Variante arbeitet die Version von Highland auf Streams. Das bedeutet, dass sie nicht sämtliche Daten auf einmal verarbeitet, sondern immer nur dann, wenn der Stream tatsächlich Daten bereitstellt:

_([ 1, 2, 3, 4, 5 ]).map(n => n * 2).toArray(squares => {
console.log(squares);
});

Was auf den ersten Blick nach einem umständlichen Weg aussieht, ein Array von Zahlen zu verdoppeln, gewinnt zunehmend an Eleganz, wenn man sich den inneren Ablauf verdeutlicht. Im Gegensatz zur in JavaScript serienmäßig enthaltenen map-Funktion wird das Array nicht schlagartig verarbeitet, sondern nach und nach.

Die Verarbeitung erfolgt als Stream. Das bedeutet, dass der gleiche Code funktionieren würde, wenn die Daten nicht aus einem Array stammen würden, sondern beispielsweise aus einer Datei – die noch geladen wird, während die Verarbeitung bereits beginnt.

Wenig effizient ist allerdings das Umwandeln der Ergebnisse in ein Array, das für die Ausgabe verwendet wird: Die Funktion toArray fungiert an der Stelle als zeitlicher und platzraubender Flaschenhals. Zum Glück kennt Highland aber noch weitere Funktionen, um die in einem Stream enthaltenen Daten zu verarbeiten. Dazu zählt beispielsweise die each-Funktion, die für jedes Element einzeln aufgerufen wird und daher nicht warten muss, bis alle Daten vorliegen:

_([ 1, 2, 3, 4, 5 ]).map(n => n * 2).each(square => {
console.log(square);
});

Wirklich interessant wird das Ganze jedoch erst, sobald man richtige Streams als Ausgangsbasis verwendet, die Daten im Verlauf der Zeit bereitstellen. Dann verbinden Highland und die reaktive Programmierung an sich nämlich die Stärken von Streams mit jenen der funktionalen Programmierung, was zu ausgesprochen kurzen und überschaubaren, zugleich aber extrem mächtigen Programmen führt.

tl;dr: Highland ist ein Modul für die reaktive Programmierung unter JavaScript und Node.js. Es verbindet die Stärken von Streams mit jenen der funktionalen Programmierung und ermöglicht auf die Art, einfachen und zugleich mächtigen Code zu schreiben. ()