Kolumne

Olis bunte Welt der IT: Funktionales JavaScript
Keine Kommentare

In der Vergangenheit habe ich mich in mehreren Kolumnen als Anhänger der funktionalen Programmierung geoutet. Da ich meinen Code am liebsten in JavaScript schreibe, ist es an der Zeit für eine Beschreibung der funktionalen Ideen in diesem Umfeld.

Es ist wohl so, dass Entwickler heutzutage durchaus Verständnis für funktionale Programmierung haben, da die Ideen seit vielen Jahren überall diskutiert werden und Einzug in viele Programmiersprachen gehalten haben, die traditionell eher prozedural oder objektorientiert ausgerichtet waren. Ich stelle fest, dass ich selbst bereits bei der BASTA! 2008 über funktionale Programmierung in C# vorgetragen habe. Wie die Zeit vergeht!

Mein Eindruck 2017 war allerdings, dass Programmierer nun gewissen Gruppen angehören. Da gibts diejenigen, die funktionale Ideen toll finden und sich entsprechend gut damit auskennen. Es gibt aber auch solche, die lieber objektorientiert denken und sich um funktionale Ideen gar keine Gedanken machen wollen.

In funktionalem Code sind Funktionen nicht Bestandteil einer übergeordneten Struktur wie einer Klasse, und sie werden unabhängig voneinander ausgeführt.

Sie wissen, ich mag Schwarz-weiß-Sichtweisen nicht, und wie so oft geht es auch funktional durchaus auf einem Mittelweg. Es geht gar nicht darum, komplett anderen Code zu schreiben – nur darum, manchmal gewisse Ansätze zu bevorzugen.

Grundlagen

Das haben Sie bestimmt schon mal jemanden sagen hören: Bei der funktionalen Programmierung geht es um Modularisierung mit Funktionen. Funktionen sind also Bausteine, aus denen größere Elemente zusammengesetzt werden. Diese Idee kann unterschiedlich eingängig erscheinen, je nachdem, an was für Anwendungen Sie denken. Leicht erschließt sich das beispielsweise für ein Dienstkonzept, bei dem von außen kommende Aufrufe an Funktionen erzeugt werden, die zustandslos und unabhängig voneinander sind – Klassen braucht man in einem solchen System offensichtlich nicht. Bitte denken Sie, wenn ich von Klassen schreibe, nicht an Datenmodelle! Auch strikt funktionale Sprachen bieten Mechanismen zur Modellierung von Daten. Mir geht es hier um die Frage, wo Funktionen „leben“, in welchem Kontext sie ausgeführt werden. In funktionalem Code sind Funktionen nicht Bestandteil einer übergeordneten Struktur wie einer Klasse, und sie werden unabhängig voneinander ausgeführt, allenfalls im Kontext einer anderen Funktion, wenn Closures verwendet werden.

BASTA! 2018

Elegante und performante WebAPIs und Webanwendungen (MVC & Razor Pages) mit ASP.NET Core 2.1?

mit Dr. Holger Schwichtenberg (IT-Visions.de / 5Minds IT-Solutions.de)

Machine Learning mit TensorFlow

mit Max Kleiner (kleiner kommunikation)

Um Funktionen als Bausteine verwenden zu können, empfiehlt es sich, gewisse Dinge bei der Erstellung von Funktionen zu beachten. Wichtig ist zum Beispiel das Konzept Partial Application, mit dem die Idee gemeint ist, einer Funktion zunächst nur manche ihrer Parameter zu übergeben und andere für später vorzuhalten. Zur Illustration nehmen Sie einmal an, Sie hätten vor, eine Liste von Werten nach einem Bereich zu filtern:

const values = [1, 7, 11, 25, 77, 384, 9808];
const filtered = values.filter(x => x >= 30 && x <= 500);
// filtered is now [77, 384]

Der Code verwendet die Higher Order Function filter, die JavaScript für das Array anbietet, und ein Prädikat in Form des Lambdaausdrucks, mit dessen Hilfe festgestellt wird, ob sich ein einzelner Wert aus der Liste im gewünschten Bereich befindet.

Nun wäre es schön, dieses Konzept von Werten in Bereichen etwas zu verallgemeinern. Zum Beispiel könnten Sie eine Funktion für das Prädikat schreiben:

const between = (value, start, end) => value >= start && value <= end;
const filtered = values.filter(x => between(x, 30, 500));

Das ist schon nicht schlecht. Allerdings finden sich hier die Parameter für den relevanten Bereich noch im Funktionsaufruf, was Lesbarkeit und Pflegbarkeit beeinträchtigt. Eigentlich ist es ja so, dass die Parameter zur Festlegung des Bereichs schon vorher feststehen und sich für diese Filteroperation oder gar für den gesamten Programmablauf gar nicht ändern. Mit dem folgendem Code lässt sich die Sache noch besser darstellen:

const between = (start, end) => value => value >= start && value <= end;
const relevantRange = between(30, 500);
const filtered = values.filter(relevantRange);

Im Beispiel habe ich start und end zusammengefasst. Das kann man auch machen, wenn die beiden Werte aus logischen Gründen grundsätzlich gemeinsam übergeben werden müssen. Technisch sauberer ist es, eine Form für die Funktion zu wählen, in der jeder Parameter einzeln übergeben wird:

const between = start => end => value => value >= start && value <= end;
const relevantRange = between(30)(500);

Ein kleiner Tipp: Wenn Sie sich fragen, warum das überhaupt funktioniert, empfehle ich, eine längere Form der Funktion zu betrachten.

function between(start) {
  return function(end) {
    return function(value) {
      return value >= start && value <= end;
    };
  };
}

In dieser Form lässt sich leicht erkennen, wie jede Funktion nur einen Parameter entgegennimmt und ihrerseits eine Funktion zurückliefert, bis letztlich alle Werte verfügbar sind, um die eigentliche Auswertung durchzuführen.

Currying

Der Vorgang, eine Funktion mit mehreren Parametern in eine zu überführen, die nur jeweils einen Parameter gleichzeitig entgegennimmt, wird nach dem Mathematiker Haskell Curry als „Currying“ bezeichnet. Wenn eine Funktion im „gecurryten“ Format aufgerufen wird, aber nicht alle Parameter gleichzeitig übergeben werden, nennt man das Partial Application.

Für Partial Application ist offensichtlich wichtig, in welcher Reihenfolge Parameter angegeben sind. Ohne Weiteres werden Parameter immer „von links“ übergeben. Ich habe im obigen Beispiel den Parameter value für die Funktion between ans Ende der Liste verschoben, da es mir am wahrscheinlichsten erschien, dass dieser Wert letztlich bei Partial Application „offen bleiben“ würde. Natürlich lassen sich auch verschiedene Varianten einer Funktion erzeugen:

const originalBetween = (value, start, end) => value >= start && value <= end;
const fullyCurriedBetween = start => end => value =>
  originalBetween(value, start, end);
const otherVersionBetween = (start, end) => value =>
  originalBetween(value, start, end);

Für die praktische Verwendung von Mechanismen wie Currying mit beliebigen Funktionen empfehle ich die Nutzung von hilfreichen Libraries. Meine persönlichen Favoriten sind Lodash und Ramda – beide bieten eine ähnliche Funktionalität, soweit es diesen Artikel betrifft. Mit Lodash etwa lässt sich das letzte Beispiel wie folgt schreiben:

const fullyCurriedBetween = _.curry(originalBetween);
const relevantRange = fullyCurriedBetween(_, 30, 500);

Hilfsfunktionen wie curry benötigen Sie natürlich hauptsächlich zum Umgang mit Funktionen, die nicht bereits im „richtigen“ Format vorliegen. Die angesprochenen Libraries enthalten aber auch andere nützliche Hilfsmittel, im Falle von Lodash hauptsächlich im Modul fp. Zum Beispiel gibt es da Varianten der Standard-Higher-Order-Functions map, filter und reduce, die für Partial Application besser geeignet sind als die „normalen“ Implementierungen in array.

const _ = require('lodash/fp');

const between = start => end => value => value >= start && value <= end;
const relevantRange = between(30)(500);
const rangeFilter = _.filter(relevantRange);

const filtered1 = rangeFilter(values);
// reuse rangeFilter anywhere

Und dann zusammenbauen

Das letzte wichtige Hilfsmittel auf dem Weg zum funktionalen Glück ist die Komposition. Die Idee basiert darauf, dass bei einer schrittweisen Verarbeitung von Werten oft mehrere Funktionen jeweils mit Parametern aufgerufen werden, die zuvor von einer anderen Funktion zurückgegeben wurden. Zum Beispiel, angeschlossen an den rangeFilter aus dem vorherigen Listing:

const sumOfValues = _.reduce((r, v) => r + v);
const sumOfRangeValues = vs => sumOfValues(rangeFilter(vs));

// returns 77+384=461
const sum = sumOfRangeValues(values);

In sumOfRangeValues werden die beiden vorhandenen Funktionen sumOfValues und rangeFilter kombiniert. Dies kann allerdings auch automatisiert werden, etwa mit Lodash:

const sumOfRangeValues = _.compose([sumOfValues, rangeFilter]);

Hierum geht es also bei der Idee der funktionalen Programmierung. Aufgabenstellungen werden in einfache Vorgänge zerlegt, die jeweils von einer Funktion gehandhabt werden können. Falls es eine Standardfunktion gibt, mit deren Hilfe die Implementierung erleichtert werden kann, wird diese eingesetzt – möglichst, wie in den Beispielen, mithilfe von Partial Application. Derselbe Mechanismus, gepaart mit Komposition, wird dann benutzt, um die einzelnen Teile der Logik in größere Funktionen zusammenzufassen. Natürlich lassen sich sowohl die kleinen als auch die großen Teile wunderbar automatisch testen, da alles absolut zustandsfrei abläuft.

Funktionale Programmierung ist überall

Um auf jedes andere relevante Teilthema der funktionalen Programmierung einzugehen, fehlt mir hier der Platz. Ich möchte allerdings noch einige Bereiche ansprechen, in denen die beschriebenen funktionalen Ansätze zu finden sind. Erstes Beispiel: Promises. Dieses Pattern passt wunderbar zur funktionalen Programmierung, etwa so:

const sumOfQueryData = queryData =>
  getData(queryData) // presumably this returns a promise
    .then(rangeFilter)
    .then(sumOfValues);

Da brauchen Sie auch gar kein async/await – tatsächlich lässt sich der funktionale Ansatz mit einfachen then-Ketten besser darstellen.

Auch im Bereich React und Redux ist funktionale Programmierung zu Hause. Mit Redux werden gern Datentypen verwendet, die unveränderbar sind – damit lassen sich Kommandos und Zustände am besten darstellen. Ein Reducer in Redux ist eine Funktion, die aus einem vorherigen Zustand und einer Aktion den nächsten Zustand berechnet. Das entspricht genau der Funktion, die an die Higher Order Function reduce übergeben wird, wie oben:

const sumOfValues = _.reduce((r, v) => r + v);

Tatsächlich hat die Entwicklung in JavaScript sich auf breiter Front bereits so weit in diese Richtung bewegt, dass Sie sich mit funktionalen Techniken sehr wohlfühlen werden.

Wenn Sie also Redux verwenden und Reducer implementieren, füllen Sie praktisch die Lücken in einem funktionalen Ausführungspfad, der anhand von Aktionen Zustände manipuliert, ohne dabei jemals veränderbare Datentypen zu verwenden.

React selbst ermöglicht von Haus aus die Erzeugung von Komponenten mithilfe von Funktionen statt Klassen:

const ErrorPanel = ({ message }) => (
  <Alert bsStyle="danger">
    <p>{message}</p>
  </Alert>
);

Allerdings gibt es keine eingebauten Mechanismen, um etwa solche Komponenten mit Zustandsinformationen zu versehen. Es gibt allerdings die hervorragende Library Recompose, die mit diversen Hilfsfunktionen diese Lücken schließt und die vollständig funktionale Implementierung komplexer React-Komponenten ermöglicht (Listing 1).

import { branch, renderNothing, withState, compose } from 'recompose';

const hideIfInvisible = isInvisible => branch(isInvisible, renderNothing);
const withVisible = withState('visible', 'setVisible', true);

const enhance = compose(
  withVisible,
  hideIfInvisible(({ visible }) => !visible)
);

const ErrorPanel = enhance(({ message, setVisible }) => (
  <Alert bsStyle="danger" onDismiss={() => setVisible(false)}>
    <p>{message}</p>
  </Alert>
));

Mit diesem Ansatz werden bestimmte Verhaltensweisen, die sich auf Komponenten anwenden lassen, in Funktionen kodiert. Diese Funktionen bezeichnen wir als „Higher Order Components“ (HOC), da sie React-Komponenten entgegennehmen und wiederum neue Komponenten zurückgeben, die gegenüber dem Original modifiziert sind. Im Beispiel wird etwa eine HOC-Funktion withVisible erzeugt, die ein Zustandsfeld hinzufügt, und eine Funktion hideIfInvisible, die das Rendering der Komponente durch nichts (renderNothing) ersetzt, wenn diese nicht sichtbar ist. Diese HOC-Funktionen sind in hohem Maße wiederverwendbar.

Mithilfe von compose können dann wie zuvor verschiedene Teiloperationen für bestimmte Einsatzfälle zusammengebaut werden. In der Komponente, die letztlich an die Funktion enhance im Beispiel übergeben wird, sehen Sie die Verwendung des „von außen“ stammenden Statusfelds setVisible.

Fazit

Ich hoffe, es ist mir gelungen, Ihnen in dieser Kolumne einen Eindruck zu verschaffen, worum es beim Einsatz funktionaler Ideen in JavaScript eigentlich geht. Mir war es außerdem wichtig zu zeigen, dass diese Ansätze nicht in ihrer eigenen Welt leben, in der ein Programmierer alles ganz anders macht als der Rest der Welt. Tatsächlich hat die Entwicklung in JavaScript sich auf breiter Front bereits so weit in diese Richtung bewegt, dass Sie sich mit funktionalen Techniken sehr wohlfühlen werden – probieren Sie es einmal aus!

Windows Developer

Windows DeveloperDieser Artikel ist im Windows Developer erschienen. Windows Developer informiert umfassend und herstellerneutral über neue Trends und Möglichkeiten der Software- und Systementwicklung rund um Microsoft-Technologien.

Natürlich können Sie den Windows Developer über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist der Windows Developer ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -