Curry PatternSpezialisierte Funktionen erstellen
von Christian HeynEs geht hier nicht um die Zubereitung eines leckeren Gerichts. Currying ist ein Muster welches nach einem amerikanischen Mathematiker benannt ist. Es 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?
Keine Sorge 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 dann so:
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:
const add = (n, m) => (n + m);
const result = [2, 3, 5, 7, 11].map((x) => add(3, x));
Mit der curryed Variante sehe es so aus:
const add = (n) => (m) => (n + m);
const 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“.
const add = (n) => (m) => (n + m);
const add3 = add(3);
const 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:
<input
onChange={(event) => dispatch({
type: A,
payload: event.target.value
})}
/>
<input
onChange={(event) => dispatch({
type: B,
payload: event.target.value
})}
/>
<input
onChange={(event) => dispatch({
type: C,
payload: event.target.value
})}
/>
Mit Currying sieht es wie folgt aus:
const dispatchWith = (type) => (event) => dispatch({
type,
payload: event.target.value
})
<input onChange={dispatchWith(A)} />
<input onChange={dispatchWith(B)} />
<input onChange={dispatchWith(C)} />
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.
const byId = (id) => (element) => {
return (id === element.id)
|| (
('childElements' in element)
&& element.childElements.some(byId(id))
)
}
Dies könnte unser Datensatz sein:
const data = [ { id: 'Nr1' }, { id: 'Nr2', childElements: [ { id: 'Nr2.1', childElements: [ { id: 'Nr2.2' }, { id: 'Nr2.3' }, { id: 'Nr2.4' }, ] } ] }, { id: 'Nr3' }, ]
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 Produktivcode 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.
HOCsHigh-Order-Components
React Komponenten höherer Ordnung sind ein ganz eigenes Thema. Welches ich in einem anderen Artikel, im Detail, beleuchten werde. Es geht darum React-Komponenten mit Daten oder Funktionalität anzureichern. Also eine bestehende Komponente durch eine Funktion zu schieben und eine mächtigere Komponente zu erhalten. Das Grundgerüst einer HOC sieht wie folgt aus:
const myHoc = (Component) => (props) => {
return <Component {...props} />;
}
Wie man erkennt wird hier das Curry-Pattern angewandt. Wer nicht auf meinen Artikel warten möchte der kann in der offziellen React Dokumentation das Thema HOC schon anlesen.
Zusammenfassung
Ich persönlich nutze Currying schon ganz intuitiv. Oft bemerke ich beim Schreiben einer Funktion dass diese mehr als ein Argument benötigt und dass es hier sinnvoll wäre das Curry Pattern zu nutzen. Der Aufwand ist so gering. Ein Koma entfernen und zwei Klammern hinzufügen. Somit ist es auch einfach den Schritt wieder rückgängig zu machen. Sollte man merken dass Currying keinen Vorteil bringt dann zwei Klammern entfernen und ein Koma hinzufügen.
...Wie so manche Zeile zu einer menschenlesbaren Anweisung wird.
Interessant ist für mich wie es den Code verändert. Wie so manche Zeile zu einer menschenlesbaren Anweisung wird. Wie Funktionen verallgemeinert werden und ihr Wiederverwendbarkeit zunimmt. Wichtig ist dass man es nicht übertreibt. Wenn man jeden Parameter grundlos einzeln übergeben muss nervt es das gesamte Team. Zum Beispiel beim Erstellen eines Adress-Objekts in dem der Adresszusatz optional ist:
createAddress('Street 3')(undefined)('ZIP')('City');
Meine Faustregel für das Anwenden des Curry Musters: Mindestens zwei, maximal 3 Parameter. Wobei der erste Parameter häufig gleichbleibend ist und nicht nur optional übergeben werden kann.
Hast du schöne Anwendungsbereiche des Curry Patterns? Dann lass es mich gern wissen.
Du hast Fragen, Anmerkungen oder einen Fehler entdeckt?
Schreib mir gern eine E-Mail an christian-dev@mailbox.org
oder nutze direkt das folgende Formular.
Dein Feedback wird für niemanden ersichtlich sein. Deine Nachricht wird nicht auf dieser Website veröffentlicht oder weiter gereicht ohne deine schriftliche Einwilligung.
Dir gefällt der Artikel "Curry Pattern - Spezialisierte Funktionen erstellen"? Ich teile meine Expertise gern mit dir und deinem ganzen Team. Neben individuellen Workshops biete ich auch den Workshop "Produkt-Entwicklung mit React" an. Wenn das interessant für dich klingt dann lass uns gern persönlich sprechen.