Skip to content

Kapitel 04: Currying

Can't Live If Livin' Is without You

Mein Vater erklärte einmal, dass man einige Dinge entbehren kann, bis man sie besitzt. Eine Mikrowelle ist so ein Beispiel. Smartphones ein anderes. Ältere Semester erinnern sich noch an ein erfülltes Leben ohne Internet. Für mich gehört Currying in diese Kategorie.

Das Konzept ist einfach: Man kann eine Funktion mit weniger Argumenten aufrufen als erwartet. Sie gibt eine Funktion zurück, die die verbleibenden Argumente entgegennimmt.

Man kann entweder alle Argumente auf einmal übergeben oder schrittweise einzelne nachreichen.

js
const add = x => y => x + y;
const increment = add(1);
const addTen = add(10);

increment(2); // 3
addTen(2); // 12

Hier haben wir eine Funktion add erstellt, die ein Argument nimmt und eine Funktion zurückgibt. Durch den Aufruf merkt sich die zurückgegebene Funktion dank Closure das erste Argument. Das gleichzeitige Übergeben beider Argumente ist jedoch umständlich. Mit der Hilfsfunktion curry wird die Definition und Verwendung solcher Funktionen erleichtert.

Richten wir einige curryste Funktionen zum Experimentieren ein. Von nun an verwenden wir unsere in Anhang A - Wesentliche Funktionsunterstützung definierte curry-Funktion.

js
const match = curry((what, s) => s.match(what));
const replace = curry((what, replacement, s) => s.replace(what, replacement));
const filter = curry((f, xs) => xs.filter(f));
const map = curry((f, xs) => xs.map(f));

Das hier verwendete Muster ist simpel, aber entscheidend: Die zu verarbeitenden Daten (String, Array) werden strategisch als letztes Argument positioniert. Der Grund hierfür wird bei der Anwendung deutlich.

(Die Syntax /r/g ist ein regulärer Ausdruck für alle Buchstaben 'r' finden. Mehr zu regulären Ausdrücken bei Interesse.)

js
match(/r/g, 'hello world'); // [ 'r' ]

const hasLetterR = match(/r/g); // x => x.match(/r/g)
hasLetterR('hello world'); // [ 'r' ]
hasLetterR('just j and s and t etc'); // null

filter(hasLetterR, ['rock and roll', 'smooth jazz']); // ['rock and roll']

const removeStringsWithoutRs = filter(hasLetterR); // xs => xs.filter(x => x.match(/r/g))
removeStringsWithoutRs(['rock and roll', 'smooth jazz', 'drum circle']); // ['rock and roll', 'drum circle']

const noVowels = replace(/[aeiou]/ig); // (r,x) => x.replace(/[aeiou]/ig, r)
const censored = noVowels('*'); // x => x.replace(/[aeiou]/ig, '*')
censored('Chocolate Rain'); // 'Ch*c*l*t* R**n'

Hier zeigt sich die Möglichkeit, Argumente in einer Funktion vorab zu laden, um neue Funktionen mit gespeicherten Parametern zu erhalten.

Ich empfehle das Klonen des Mostly-Adequate-Repositories (git clone https://github.com/MostlyAdequate/mostly-adequate-guide.git), den obigen Code einzufügen und im REPL auszuprobieren. Die curry-Funktion und alle Anhang-Definitionen finden sich im Modul support/index.js.

Oder eine veröffentlichte Version auf npm verwenden:

npm install @mostly-adequate/support

Mehr als ein Wortspiel / Die geheime Zutat

Currying ist vielfältig einsetzbar. Mit Basisfunktionen und Parametern lassen sich neue Funktionen wie hasLetterR, removeStringsWithoutRs und censored generieren.

Durch Umhüllen mit map wird jede Einzelelement-Funktion zu einer Array-Funktion:

js
const getChildren = x => x.childNodes;
const allTheChildren = map(getChildren);

Das Aufrufen mit weniger Argumenten nennt man partielle Anwendung. Dies reduziert Boilerplate-Code. Vergleichen Sie die curryste allTheChildren-Funktion mit der uncurried lodash-map (beachte andere Argumentreihenfolge):

js
const allTheChildren = elements => map(elements, getChildren);

Array-spezifische Funktionen sind oft überflüssig, da map(getChildren) direkt aufgerufen werden kann. Das gilt auch für sort, filter und andere Higher-Order-Funktionen (Funktionen, die Funktionen entgegennehmen/ausgeben).

Reine Funktionen bilden laut Definition 1 Eingabe auf 1 Ausgabe ab. Currying erfüllt dies genau: Jedes Argument liefert eine neue Funktion für die restlichen Parameter. Das, werter Leser, ist 1 Eingabe zu 1 Ausgabe.

Auch wenn die Ausgabe eine Funktion ist - sie gilt als rein. Mehrere Argumente auf einmal sind zulässig, dienen aber nur der Vereinfachung durch Wegfall zusätzlicher ().

Zusammenfassung

Currying ist praktisch und ich arbeite täglich gerne mit currysten Funktionen. Es ist ein Werkzeug, das funktionale Programmierung weniger umständlich macht.

Durch partielles Übergeben von Argumenten lassen sich blitzschnell nützliche Funktionen erstellen. Dabei bleibt die mathematische Funktionsdefinition trotz Mehrfachargumenten erhalten.

Kommen wir zum nächsten essenziellen Werkzeug: compose.

Kapitel 05: Programmieren durch Komposition

Übungen

Hinweis zu Übungen

In diesem Buch finden Sie Übungsabschnitte wie diesen. Diese können direkt im Browser durchgeführt werden, wenn Sie die GitBook-Version lesen (empfohlen).

Beachten Sie: Für alle Übungen stehen globale Hilfsfunktionen aus Anhang A, B und C zur Verfügung. Zusätzlich definieren einige Übungen problemspezifische Funktionen - diese sind ebenfalls verfügbar.

Tipp: Lösungen mit Strg + Enter im Editor einreichen!

Übungen lokal ausführen (optional)

Für die Bearbeitung im eigenen Editor:

  • Repository klonen (git clone git@github.com:MostlyAdequate/mostly-adequate-guide.git)
  • In den Übungsordner navigieren (cd mostly-adequate-guide/exercises)
  • Empfohlene Node-Version v10.22.1 verwenden (z.B. nvm install). Details im Buch-Readme
  • Abhängigkeiten mit npm installieren (npm install)
  • Aufgaben durch Bearbeiten der exercise_*-Dateien im jeweiligen Kapitelordner lösen
  • Korrektur mit npm ausführen (z.B. npm run ch04)

Unit Tests überprüfen Lösungen und geben Hinweise bei Fehlern. Musterlösungen sind in *solution_**-Dateien enthalten.

Los geht's!

Let's Practice!

Refaktorisieren Sie die Funktion durch partielle Anwendung, um alle Argumente zu entfernen. const words = str => split(' ', str);


Let's Practice!

Entfernen Sie durch partielles Anwenden der Funktionen alle Argumente. const filterQs = xs => filter(x => match(/q/i, x), xs);


Gegeben sei folgende Funktion:

js
const keepHighest = (x, y) => (x >= y ? x : y);

Let's Practice!

Refaktorisieren Sie max so, dass ohne Argumentreferenzen unter Verwendung der Hilfsfunktion keepHighest gearbeitet wird. const max = xs => reduce((acc, x) => (x >= acc ? x : acc), -Infinity, xs);