Currying

Lesezeit: 10 Minuten

Currying ist eine Technik, um eine Funktion mit mehreren Argumenten in eine Abfolge von Funktionen mit jeweils nur einem Argument umzuwandeln.

Currying ist nach dem US-amerikanischen Mathematiker Haskell Brooks Curry benannt, der sich zwar ausführlich damit auseinandergesetzt, das Verfahren aber tatsächlich nicht erfunden hat. Als Erfinder gilt Moses Schönfinkel, der wiederum auf den Ideen von Gottlob Frege aufgesetzt hat. Currying wird daher gelegentlich auch als Schönfinkeln bezeichnet.

Anwendung in verschiedenen Programmiersprachen

Currying lässt sich in allen Programmiersprachen umsetzen, die Unterstützung für Higher-Order Functions und Closures bieten. Eine Funktion mit drei Argumenten, die in der Form f(a, b, c) aufgerufen werden kann, wird nach Anwendung von Currying in der Form g(a)(b)(c) aufgerufen. Dabei liefert g(a) eine Funktion h zurück, h(b) eine Funktion i zurück und i(c) das gleiche Ergebnis wie f(a, b, c). Dabei haben die inneren Funktionen Zugriff auf die äußeren Kontexte, die Funktion i kann also nicht nur auf c, sondern auch auf a und b zugreifen.

d = f(a, b, c)

h = g(a)
i = h(b)
d = i(c)

f(a, b, c) = g(a)(b)(c)

Die folgenden Beispiele zeigen, wie eine Funktion productOfThree, die für productOfThree(a, b, c) = a * b * c berechnet, mit Anwendung von Currying in verschiedenen Sprachen umgesetzt werden kann.

Haskell

ghci> productOfThree = \a -> \b -> \c -> a * b * c
ghci> productOfThree 3 4 5
60                       
ghci> productOf3And4And = productOfThree 3 4
ghci> productOf3And4And 5
60                       
ghci> productOfThree' a b c = a * b * c
ghci> productOfThree' 3 4 5
60
ghci> productOf3And4And' = productOfThree' 3 4
ghci> productOf3And4And' 5
60

Die Funktion productOfThree ist eine Aneinanderreihung dreier Lambda-Funktionen. Die Funktion productOf3And4And ist eine Referenz auf die innerste Funktion, in der a=3 und b=4 bereits gesetzt sind.

Die Zeilen 7 bis 12 demonstrieren, dass in Haskell tatsächlich jede Funktion ge-curried wird: Jeder Aufruf einer Haskell-Funktion mit weniger als der Anzahl der erforderlichen Argumente liefert eine Funktion zurück, die das jeweils nächste Argument erwartet. Es ist also gar nicht notwendig, explizit die Lambda-Schreibweise zu verwenden.

JavaScript

> productOfThree = a => b => c => a * b * c
[Function: productOfThree]
> productOfThree(3)(4)(5)
60
> productOf3And4And = productOfThree(3)(4)
[Function (anonymous)]
> productOf3And4And(5)
60

Die Arrow-Notation ermöglicht eine sehr kompakte Anwendung von Currying in JavaScript.

Kotlin

>>> val productOfThree = { a: Int -> { b: Int -> { c: Int -> a * b * c }}}
>>> productOfThree(3)(4)(5)
res2: kotlin.Int = 60
>>> val productOf3And4And = productOfThree(3)(4)
>>> productOf3And4And(5)
res4: kotlin.Int = 60

Mithilfe von Lambda-Funktionen lässt sich Currying in Kotlin als Einzeiler umsetzen.

Python

>>> productOfThree = lambda a : lambda b : lambda c : a * b * c
>>> productOfThree(3)(4)(5)
60
>>> productOf3And4And = productOfThree(3)(4)
>>> productOf3And4And(5)
60

Auch Python bietet mit Lambda-Funktionen eine kompakte Schreibweise zur Umsetzung von Currying.

Ruby

irb(main):001:0> productOfThree = lambda { |a| lambda { |b| lambda { |c| a * b * c }}}
irb(main):002:0> productOfThree.(3).(4).(5)
=> 60
irb(main):003:0> productOf3And4And = productOfThree.(3).(4)
=> #<Proc:0x0000556e2b92aab0 (irb):7 (lambda)>
irb(main):004:0> productOf3And4And.(5)
=> 60

Die Implementierung in Ruby entspricht vom Prinzip den vorangegangenen Beispielen.

Currying für Kontextinformationen

Currying ist ein Konzept der funktionalen Programmierung. Dessen Verwendung ist damit zu einem gewissen Grad eine paradigmatische Entscheidung. Currying kann z.B. eine Alternative zu lokalen Variablen darstellen, insbesondere, wenn die jeweiligen Argumente häufiger übergeben werden.

Das folgende Beispiel zeigt, wie verschiedene Datensätze für einen Mandanten gespeichert werden. Dabei wird der Mandant anhand seiner ID aufgelöst, und das dabei erzeugte Mandantenobjekt wiederholt an eine Funktion insertData übergeben, um einen Kunden, eine Adresse und eine Bestellung für diesen anzulegen.

const tenant = resolveTenantById(id)
insertData(tenant, customer)
insertData(tenant, address)
insertData(tenant, order)

Das nächste Beispiel zeigt, wie eine Umsetzung mit Currying aussehen kann: Statt den Mandanten selbst in einer lokalen Variable zur Wiederverwendung vorzuhalten, wird eine neue Funktion insertDataForTenant deklariert, in die der Mandant als das erste Argument in insertData festgeschrieben wird.

const insertDataForTenant = insertData(resolveTenantById(id))
insertDataForTenant(customer)
insertDataForTenant(address)
insertDataForTenant(order)

Aus dem Currying ergibt sich der Vorteil, dass es bei Verwendung dieser Funktion nicht mehr möglich ist, unabsichtlich einen anderen Mandanten anzugeben, während es bei der Variante mit der lokalen Variable nicht obligatorisch ist, dass diese auch beim Funktionsaufruf herangezogen wird.

Weitere ähnliche Anwendungsfälle, in denen das gleiche Argument mehrere Male an eine Funktion übergeben werden, sind:

  • HTTP-Header beim Aufruf einer HTTP-API
  • Credentials beim Aufruf einer API mit Authentifizierung
  • Operationen an einem identifizierbaren Objekt, z.B. einem DOM-Element einer Webseite
  • Angabe einer Sprache bei eine API zur Übersetzung
  • Angabe von Datum oder Zeitpunkt bei einer API, die zeitabhängige Informationen liefert
  • Angabe von einem Argument mit einem sehr beschränkten Wertebereich, z.B. der Loglevel beim Logging
  • Angabe einer Umrechnung, die durch eine simple Multiplikation ausgedrückt werden kann (Meilen in Kilometer, Liter in Gallonen)

All diese Informationen beschreiben den Kontext, aber nicht den Kern von dem, was getan werden soll. Beim Aufruf der HTTP-API muss ich HTTP-Header mitgeben, im Kern möchte ich aber einen Benutzer löschen. Mit der Operation an einem DOM-Element muss ich das Objekt identifizieren, im Kern geht es mir aber darum, einen Text anzuzeigen. Die Angabe der Sprache ist für die Übersetzung notwendig, im Kern habe ich aber zum Ziel, einen Absatz zu übersetzen.

Currying fördert Separations of Concerns, indem ich diese Kontextinformationen von der eigentlichen Absicht trennen kann.

Vor- und Nachteile von Currying

Durch Currying kann Code sauberer und strukturierter werden. Currying erhöht die Wiederverwendbarkeit, reduziert Wiederholungen und fördert Komposition.

Currying selbst bietet keinen Cache oder Performance-Vorteil, da Berechnungen erst in der innersten Funktion durchgeführt werden, ähnlich wie lokale Variablen werden durch Currying aber Vorberechnungen „zwischengespeichert“, so dass sie nicht bei jedem Funktionsaufruf neu berechnet werden müssen – etwa gehashte Passwörter.

Beim Currying ist die Anzahl der Argumente von vornherein festgelegt. Die in einem vorherigen Abschnitt aufgeführte Funktion productOfThree kann nicht einfach in eine generische Funktion productOfN erweitert werden, da jede zurückgegebene Funktion vor dem letzten Argument ein weiteres Argument erwartet und vorher keine Berechnung durchgeführt wird.

Currying zeigt gewisse Parallelen zu den Entwurfsmustern Builder und Decorator, ist in dieser Funktionsweise durch die festgelegte Anzahl an Argumenten aber eingeschränkter.

Seinen größten Vorteil spielt Currying da aus, wo es wie in Haskell und F# fester Bestandteil der Sprache ist und zu einem konziseren Stil beiträgt:

ghci> map (\x -> x * 2) [1..5] -- Lambda-Variante
[2,4,6,8,10]
ghci> map (* 2) [1..5] -- kompakte Variante
[2,4,6,8,10]
ghci> multiplyAndAdd m a v = v * m + a -- drei Argumente
ghci> multiplyAndAdd 3 7 5 -- 5 * 3 + 7
22
ghci> map (multiplyAndAdd 3 7) [1..5]
[10,13,16,19,22]
-- [1 bis 5] * 3 + 7

Currying vs. Partial Function Application

Bei der Funktion productOf3And4And weiter oben handelt es sich tatsächlich um ein Beispiel für Partial Function Application (Kurzform: Partial Application). Der Begriff bezeichnet den Aufruf einer Funktion mit einem Teil der Argumente. Das folgende Beispiel demonstriert, dass es sich bei Partial Application nicht zwingend um eine Funktion mit Currying handeln muss:

> sumOfFour = (a, b) => (c, d) => a + b + c + d
[Function: sumOfFour]
> sumOfTwoAnd3And4 = sumOfFour(3, 4)
[Function (anonymous)]
> sumOfTwoAnd3And4(5, 6)
18
> sumOfFour(3, 4)(5, 6)
18

Etwas praxisnaher, aber dafür komplizierter, der Aufruf einer REST-API:

> sendHttpRequest = (a, b, c, d)
    => console.log(`sent: ${a} ${b} ${c} ${d}`)
[Function: sendHttpRequest]
> restRequest = (httpHeaders, httpMethod, path)
    => (id, requestBody)
    => sendHttpRequest(
        httpHeaders,
        httpMethod,
        `${path}/${id}`,
        requestBody
    )
[Function: restRequest]
> putUserRequest = restRequest(
    ['content-type: application/json'],
    'PUT',
    '/users'
)
[Function (anonymous)]
> putUserRequest(1, '{ "name": "Christian" }')
sent: content-type: application/json PUT /users/1 { "name": "Christian" }

Fazit

Soll ich Currying nun überall anwenden, wo es möglich ist? Auf keinen Fall! Currying ist ein Konzept aus der funktionalen Programmierung und wenn man sich mit Sprachen wie Haskell oder F# befasst, wird man automatisch darüber stolpern. Mit entsprechender Erfahrung erlangt man ein Gefühl dafür, wo der Gebrauch von Currying vorteilhaft ist und kann damit beurteilen, wann der Einsatz in objektorientierten bzw. multiparadigmatischen Sprachen wie z.B. Python infrage kommen kann. Erst dann würde ich die Verwendung in einem nicht-funktionalen Umfeld in Erwägung ziehen und dabei immer an das Principle of Least Surprise denken: Ist die Anwendung von Currying in einem Projekt total unüblich und bietet nur marginale Vorteile gegenüber z.B. lokalen Variablen, dann lässt man besser die Finger davon.