Curry Pattern in JSSpezialisierte Funktionen erstellen

Von Christian Wünsche | Lesedauer ~ 5 Minuten

Curry ist nicht nur ein vorzügliches Gericht. Es ist zudem ein Muster welches nach einem amerikanischen Mathematiker benannt ist. (Mathematik ist für die folgenden Zeilen jedoch nicht von Nöten.) Currying hilft uns Funktionen wiederzuverwenden und unseren Code ein wenig aussagekräftiger zu machen.

Haskell Brooks Curry griff die mathematische Arbeit eines gewissen Moses Schönfinkels auf und konkretisierte das Verfahren im Jahre 1958 umfangreich. Auch die Programmiersprache Haskell ist nach Haskell Brooks Curry benannt. In Haskell ist das Currying ein nativer Bestandteil und muss nicht, wie in JavaScript, simuliert werden.

Also worum geht es?

Es geht nicht um Mathematik. Es geht um ein Verfahren mit dem wir eine Funktion spezialisieren können. Was das bedeutet möchte ich direkt an einem Beispiel aufzeigen.

const add = (n, m) => (n + m);

Die Funktion

add
erwartet zwei Argumente und addiert sie. Ja, ein sehr simples Beispiel. Die selbe Funktion in der Curry-Schreibweise sieht so aus:

const add = (n) => (m) => (n + m);

Nun ist

add
eine Funktion die ein einziges Argument
n
entgegennimmt und eine neue Funktion liefert. Diese wiederum nimmt das „fehlende“
m
als Argument und liefert das Ergebnis.

Um den Vorteil zu beschreiben bleiben wir bei unserer

add
Funktion. Nehmen wir an für jede Zahl in einem Array wollen wir 3 addieren. Ohne Currying würde das so aussehen:

1const add    = (n, m) => (n + m);
2const result = [2, 3, 5, 7, 11].map((x) => add(3, x));

Mit der curryed Variante sehe es so aus:

1const add    = (n) => (m) => (n + m);
2const result = [2, 3, 5, 7, 11].map(add(3));

In beiden Fällen liefert result das Array

[5, 6, 8, 10, 14]
.

Wir haben also eine spezialisierte Funktion erstellt indem wir

add
nur das erste Argument, die
3
, übergeben haben. Sollten wir diese spezialisierte Funktion häufiger benötigen könnten wir sie auch vorab „fixieren“.

1const add  = (n) => (m) => (n + m);
2const add3 = add(3);
3const result = [2, 3, 5, 7, 11].map(add3);

Das Schöne an diesem Verfahren ist dass wir somit weniger Code schreiben. Und es wäre schnell erledigt wenn wir noch andere spezialisierte Funktionen benötigen:

/* ... */
const add3 = add(3);
const add5 = add(5);
/* ... */

Neben der gesparten Schreibarbeit entsteht auch eine gewisse Lesbarkeit.

.map(add3)
ist ziemlich aussagekräftig. Weiterhin ist es nicht mehr möglich andere Dinge darin unterzubringen wie es in der Variante ohne Currying möglich wäre:

const result = [2, 3, 5, 7, 11].map((x) => add(3, x - 1));

Sollte also ein unvorhergesehenes Verhalten auftreten kann man sicher sein dass es nicht aus der Zeile

.map(add3)
herrührt. Wenn dann muss man die Ausgangsfunktion, in unserem Fall
add
, prüfen.

Dieses Zahlen-Array Beispiel scheint wenig Bezug zu echten Projekten zu haben. Deshalb kommen nun Anwendungsbeispiele die von meiner täglichen Arbeit inspiriert sind:

Arrays filtern

Angenommen wir haben ein

user
Array. Jeder user hat einen anderen Tarif und wir benötigen an mehreren Stellen eine Filtermöglichkeit um diese Daten zu unterscheiden:

const hasPlan = (planId) => (user) => (user.plan === planId)

Wenn ich nun ein

user
Array filtern muss, nach einem bestimmten Tarif, kann ich die selbe Funktion für jeden Tarif verwenden:

users.filter(hasPlan('FREE'))
users.filter(hasPlan('PRO'))

Spezialisierte reducer-dispatch Funktionen

Oft kommt es im React-Umfeld vor dass ich für verschiedene Texteingabefelder ähnliche Reducer-Actions feuern muss. Ohne Currying sieht das so aus:

1return <>
2    <input
3        onChange={(event) => dispatch({
4            type: A,
5            payload: event.target.value
6        })}
7    />
8    <input
9        onChange={(event) => dispatch({
10            type: B,
11            payload: event.target.value
12        })}
13    />
14    <input
15        onChange={(event) => dispatch({
16            type: C,
17            payload: event.target.value
18        })}
19    />
20</>

Mit Currying sieht es wie folgt aus:

1const dispatchWith = (type) => (event) => dispatch({
2    type,
3    payload: event.target.value
4})
5
6return <>
7    <input onChange={dispatchWith(A)} />
8    <input onChange={dispatchWith(B)} />
9    <input onChange={dispatchWith(C)} />
10</>

Rekursion mit einem unverändertem Parameter

Nicht selten kommt es vor dass ich prüfen muss ob in einer mehrdimensionale Datenstruktur ein Element mit einer bestimmten

id
enthalten ist. Egal wie tief ich in den Daten suche, die zu suchende
id
bleibt gleich.

1const byId = (id) => (element) => {
2    return (id === element.id)
3    || (
4        ('childElements' in element)
5        && element.childElements.some(byId(id))
6       )
7}

Dies könnte unser Datensatz sein:

1const data = [
2    { id: 'Nr1' },
3    { id: 'Nr2', childElements: [
4        { id: 'Nr2.1', childElements: [
5            { id: 'Nr2.2' },
6            { id: 'Nr2.3' },
7            { id: 'Nr2.4' },
8        ] }
9    ] },
10    { id: 'Nr3' },
11]

Und so wird das ganze genutzt:

const idExists = data.some(byId('Nr2.3'))
// -> true

Dependency Injection

Am besten kann man eine Funktion testen wenn sie "pure" ist. Bedeutet dass man immer den selben Rückgabewert bekommt wenn man die selben Argumente hineingibt. Doch manche Funktion benötigt eben Dinge wie den aktuellen Timestamp. Sprich, der Rückgabewert der Funktion ist immer unterschiedlich da die Zeit natürlich weiter fortschreitet. Es gibt in Test-Frameworks die Möglichkeit einige globale Funktionen zu mocken. Doch wir können auch das Erstellen des Timestamps als Parameter in unsere Funktion einschleusen um im Testszenario das Mocken zu vereinfachen.

export const getTimestamp = () => Date.now();

export const getDayName = (getTimestamp, languageKey) => {
    const currentTimestamp = getTimestamp();
    /** do something with currentTimestamp & languageKey */
    /** ... */
};

Dies hat zur Folge dass wir nun immer auch die Funktion getTimestamp als Parameter übergeben müssen um unsere Funktion zu nutzen.

Im Testfall ist dass sehr angenehm. Denn wir können nun fixierte Werte testen:

describe('getDayName', () => {
    it('works!', () => {
        const getTimestamp_MOCK = () => '1625310165520';

        const actual = getDayName(getTimestamp_MOCK, 'de')
        const expected = 'Samstag';
        expect(actual).toEqual(expected);
    });
});

Doch der Nachteil ist dass wir im Projektcode auch immer diese

getTimestamp
Funktion mitschleppen und übergeben müssen. Eine Möglichkeit dass zu umgehen ist, du wirst es nicht glauben, Currying. Zu diesem Zweck exportiere ich eine weitere Funktionen aus der Datei in der vorher nur
getDayName
exportiert wurde.

export const getTimestamp = () => Date.now();

export const _getDayName = (getTimestamp) => (languageKey) => {
    const currentTimestamp = getTimestamp();
    /** do something with currentTimestamp & languageKey */
    /** ... */
};

export const getDayName = _getDayName(getTimestamp);

In unserem Test würde ich nun _getDayName nutzen um unsere

getTimestamp_MOCK
genauso zu nutzen wie schon abgebildet. Doch in meinem Produktivcode nutze ich die Funktion
getDayName
. Dieser Funktion muss ich nur noch den gewünschten
languageKey
übergeben und alles andere ist schon gegeben.

Abschließende Worte

Interessant ist für mich wie dieses Muster den Code verändert. Wie so manche Zeile zu einer menschenlesbaren Anweisung wird. Wie Funktionen verallgemeinert werden und ihr Wiederverwendbarkeit zunimmt.

FPJavaScript