Skip to content

Kapitel 05: Programmieren durch Komposition

Funktionale Komposition

Hier ist compose:

js
const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];

... Keine Sorge! Dies ist die Level-9000-Super-Saiyan-Form von compose. Zur besseren Verständlichkeit betrachten wir eine vereinfachte Version, die zwei Funktionen kombiniert. Sobald dies verstanden ist, lässt sich die Abstraktion auf beliebig viele Funktionen erweitern (was wir sogar beweisen könnten)! Hier eine verständlichere compose-Version für meine lieben Leser:

js
const compose2 = (f, g) => x => f(g(x));

f und g sind Funktionen und x ist der Wert, der durch sie „durchgeleitet“ wird.

Komposition ähnelt der Funktionszucht. Als Züchter von Funktionen wählt man zwei mit gewünschten Eigenschaften aus und kombiniert sie zu einer neuen. Anwendungsbeispiel:

js
const toUpperCase = x => x.toUpperCase();
const exclaim = x => `${x}!`;
const shout = compose(exclaim, toUpperCase);

shout('send in the clowns'); // "SEND IN THE CLOWNS!"

Die Komposition zweier Funktionen erzeugt eine neue Funktion. Das ergibt Sinn: Die Verknüpfung zweier Einheiten desselben Typs (hier Funktionen) sollte eine neue Einheit dieses Typs ergeben. Man fügt keine LEGO-Steine zusammen, um Holzbausteine zu erhalten. Hier wirkt ein theoretisches Grundprinzip, das wir noch enthüllen werden.

In unserer compose-Definition wird g vor f ausgeführt, was einen Rechts-nach-links-Datenfluss erzeugt. Dies ist lesbarer als verschachtelte Funktionsaufrufe. Ohne compose würde der Code lauten:

js
const shout = x => exclaim(toUpperCase(x));

Statt von innen nach außen arbeiten wir von rechts nach links - was ich als Schritt in die linke Richtung bezeichnen würde (Buh!). Betrachten wir ein Beispiel mit Reihenfolgeabhängigkeit:

js
const head = x => x[0];
const reverse = reduce((acc, x) => [x, ...acc], []);
const last = compose(head, reverse);

last(['jumpkick', 'roundhouse', 'uppercut']); // 'uppercut'

reverse dreht die Liste um, während head das erste Element nimmt. Dies ergibt eine effektive, wenn auch ineffiziente last-Funktion. Die Funktionsreihenfolge in der Komposition wird hier deutlich. Wir könnten eine Links-nach-rechts-Variante definieren, aber die aktuelle Form entspricht genauer der mathematischen Notation. Tatsächlich stammt diese Kompositionsregel direkt aus der Mathematik.

js
// associativity
compose(f, compose(g, h)) === compose(compose(f, g), h);

Komposition ist assoziativ, was bedeutet, dass die Gruppierung irrelevant ist. Für die Großschreibung eines Strings schreiben wir daher:

js
compose(toUpperCase, compose(head, reverse));
// or
compose(compose(toUpperCase, head), reverse);

Da die Gruppierung keine Rolle spielt, können wir eine variadische compose-Funktion verwenden:

js
// previously we'd have to write two composes, but since it's associative, 
// we can give compose as many fn's as we like and let it decide how to group them.
const arg = ['jumpkick', 'roundhouse', 'uppercut'];
const lastUpper = compose(toUpperCase, head, reverse);
const loudLastUpper = compose(exclaim, toUpperCase, head, reverse);

lastUpper(arg); // 'UPPERCUT'
loudLastUpper(arg); // 'UPPERCUT!'

Die Assoziativität ermöglicht diese Flexibilität. Die leicht komplexere variadische Version ist in den Support-Bibliotheken dieses Buchs sowie in lodash, underscore und ramda implementiert.

Ein Vorteil der Assoziativität ist, dass Funktionsgruppen extrahiert und gebündelt werden können. Refactoring-Beispiel:

js
const loudLastUpper = compose(exclaim, toUpperCase, head, reverse);

// -- or ---------------------------------------------------------------

const last = compose(head, reverse);
const loudLastUpper = compose(exclaim, toUpperCase, last);

// -- or ---------------------------------------------------------------

const last = compose(head, reverse);
const angry = compose(exclaim, toUpperCase);
const loudLastUpper = compose(angry, last);

// more variations...

Es gibt kein Richtig oder Falsch - wie LEGO-Steine können Funktionen beliebig kombiniert werden. Üblich ist die Gruppierung in wiederverwendbare Einheiten wie last und angry. Kenner von Fowlers »Refactoring« erkennen hier das »Extract Function«-Muster - ohne die Komplexität von Objektzuständen.

Punktfrei

Punktfreier Stil bedeutet, dass Funktionen ihre operierten Daten nie explizit erwähnen. Erstklassige Funktionen, Currying und Komposition ermöglichen diesen Stil.

Hinweis: Punktfreie Versionen von replace & toLowerCase sind in Anhang C - Punktfreie Hilfsmittel definiert.

js
// not pointfree because we mention the data: word
const snakeCase = word => word.toLowerCase().replace(/\s+/ig, '_');

// pointfree
const snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

Hier wurde replace partiell angewendet. Die Daten durchlaufen nacheinander einstellige Funktionen. Currying ermöglicht die Weiterleitung von Daten ohne explizite Nennung. Im punktfreien Code entfällt die Datenreferenz bei der Funktionskonstruktion, während in der expliziten Version unser »word« verfügbar sein muss.

Betrachten wir ein weiteres Beispiel:

js
// not pointfree because we mention the data: name
const initials = name => name.split(' ').map(compose(toUpperCase, head)).join('. ');

// pointfree
// NOTE: we use 'intercalate' from the appendix instead of 'join' introduced in Chapter 09!
const initials = compose(intercalate('. '), map(compose(toUpperCase, head)), split(' '));

initials('hunter stockton thompson'); // 'H. S. T'

Punktfreier Code fördert Kürze und Allgemeingültigkeit. Er dient als Prüfstein für funktionalen Code, da er kleine Input-Output-Funktionen voraussetzt. Eine while-Schleife lässt sich beispielsweise nicht komponieren. Doch Vorsicht: Punktfreiheit ist ein zweischneidiges Schwert und kann manchmal die Absicht verschleiern. Nicht jeder funktionale Code muss punktfrei sein - wir nutzen es wo möglich.

Debugging

Ein häufiger Fehler ist das Komponieren von map (eine Funktion mit zwei Argumenten) ohne vorheriges partielles Anwenden.

js
// wrong - we end up giving angry an array and we partially applied map with who knows what.
const latin = compose(map, angry, reverse);

latin(['frog', 'eyes']); // error

// right - each function expects 1 argument.
const latin = compose(map(angry), reverse);

latin(['frog', 'eyes']); // ['EYES!', 'FROG!'])

Bei Kompositionsproblemen hilft diese nützliche, wenn auch unreine trace-Funktion:

js
const trace = curry((tag, x) => {
  console.log(tag, x);
  return x;
});

const dasherize = compose(
  intercalate('-'),
  toLower,
  split(' '),
  replace(/\s{2,}/ig, ' '),
);

dasherize('The world is a vampire');
// TypeError: Cannot read property 'apply' of undefined

Hier liegt ein Fehler vor - verwenden wir trace:

js
const dasherize = compose(
  intercalate('-'),
  toLower,
  trace('after split'),
  split(' '),
  replace(/\s{2,}/ig, ' '),
);

dasherize('The world is a vampire');
// after split [ 'The', 'world', 'is', 'a', 'vampire' ]

Ah! Wir müssen toLower mit map anwenden, da es auf einem Array operiert.

js
const dasherize = compose(
  intercalate('-'),
  map(toLower),
  split(' '),
  replace(/\s{2,}/ig, ' '),
);

dasherize('The world is a vampire'); // 'the-world-is-a-vampire'

Die trace-Funktion ermöglicht Debugging durch Dateninspektion. Sprachen wie Haskell und PureScript bieten ähnliche Hilfsmittel.

Komposition ist unser zentrales Werkzeug zur Programmerstellung - gestützt durch die Kategorietheorie, die Korrektheit garantiert. Untersuchen wir diese Theorie.

Kategorietheorie

Die Kategorietheorie formalisiert Konzepte aus Mengenlehre, Typsystemen, Gruppentheorie und Logik. Sie beschäftigt sich mit Objekten, Morphismen und Transformationen - Analogien zur Programmierung. Vergleichstabelle der Konzepte:

category theory

Keine Sorge - Vertrautheit mit allen Begriffen wird nicht vorausgesetzt. Die Tabelle zeigt Redundanzen zwischen Disziplinen, die die Kategorietheorie vereinheitlicht.

Eine Kategorie besteht aus:

  • Einer Sammlung von Objekten
  • Einer Sammlung von Morphismen
  • Einem Kompositionskonzept für Morphismen
  • Einem ausgezeichneten Identitätsmorphismus

Diese Abstraktionsebene ermöglicht die Modellierung von Typen und Funktionen - unser aktueller Fokus.

Objekte Datentypen wie »String«, »Boolean«, »Number«, »Object«. Typen werden als Wertemengen betrachtet (z.B. »Boolean« = {true, false}). Mengentheoretische Methoden sind anwendbar.

Morphismen Unsere reinen Standardfunktionen.

Komposition Unser compose-Werkzeug. Die Assoziativität ist kategorietheoretisch erforderlich - kein Zufall.

Grafische Darstellung der Komposition:

category composition 1category composition 2

Codebeispiel:

js
const g = x => x.length;
const f = x => x === 4;
const isFourLetterWord = compose(f, g);

Identitätsmorphismus Die id-Funktion gibt Eingaben unverändert zurück:

js
const id = x => x;

»Wozu ist das nützlich?« fragen Sie sich vielleicht. Wir werden id in folgenden Kapiteln intensiv nutzen - vorerst verstehen wir sie als Funktion, die sich als normaler Wert ausgibt.

id verhält sich kompatibel zur Komposition. Für jede einstellige Funktion f gilt:

js
// identity
compose(id, f) === compose(f, id) === f;
// true

Analog zur Identitätseigenschaft bei Zahlen! Falls unklar: Keine Sorge - id wird bald allgegenwärtig sein. Sie ermöglicht punktfreien Code durch Wertersatz.

Damit haben wir eine Kategorie von Typen und Funktionen. Für Neueinsteiger bleibt der Nutzen vielleicht abstrakt - doch im Buchverlauf wird dies klarer. Jetzt erkennen wir bereits Assoziativität und Identität als zentrale Kompositionseigenschaften.

Weitere Kategorien? Gerichtete Graphen (Knoten=Objekte, Kanten=Morphismen), Zahlen mit »>=« als Morphismen (jede partielle/totale Ordnung bildet eine Kategorie). Für dieses Buch konzentrieren wir uns auf obige Definition.

Zusammenfassung

Komposition verbindet Funktionen wie Rohrleitungen. Daten fließen durch die Anwendung - reine Funktionen transformieren Input zu Output. Unterbrechungen würden die Software unbrauchbar machen.

Komposition ist unser oberstes Designprinzip - es erhält Einfachheit und Vernunft. Die Kategorietheorie beeinflusst Architektur, Nebenwirkungsmodellierung und Korrektheitssicherung.

Zeit für eine Praxisübung: Lassen Sie uns eine Beispielanwendung erstellen.

Kapitel 06: Beispielanwendung

Übungen

In den Aufgaben verwenden wir Auto-Objekte mit folgender Struktur:

js
{
  name: 'Aston Martin One-77',
  horsepower: 750,
  dollar_value: 1850000,
  in_stock: true,
}

Let's Practice!

Schreiben Sie die Funktion mit compose() um. const isLastInStock = (cars) => { const lastCar = last(cars); return prop('in_stock', lastCar); };


Gegeben folgende Funktion:

js
const average = xs => reduce(add, 0, xs) / xs.length;

Let's Practice!

Refaktorisieren Sie »averageDollarValue« mit »average« als Komposition. const averageDollarValue = (cars) => { const dollarValues = map(c => c.dollar_value, cars); return average(dollarValues); };


Let's Practice!

Refaktorisieren Sie »fastestCar« punktfrei mit compose(). Tipp: »append« ist nützlich. const fastestCar = (cars) => { const sorted = sortBy(car => car.horsepower, cars); const fastest = last(sorted); return concat(fastest.name, ' is the fastest'); };