Currying is yet another concept often associated with functional programming. It's close to Partial application which we discussed earlier, but not the same.
Currying is a technique of converting a function that takes multiple arguments into a chain of functions that each takes a single argument.
Currying usage could look like this in Go:
f := func(a int, b bool, c float64) string {
return fmt.Sprint(a) + " " + fmt.Sprint(b) + " " + fmt.Sprint(c)
}
curriedF := Curry3(f)
r := curriedF(1)(true)(5.5)
fmt.Println(r)
// Output: 1 true 5.5
Curry3
is a helper function provided by Go4Fun library that takes a 3-argument function as a parameter and does Currying: it converts a provided function of 3 arguments into a chain of 3 functions each taking exactly 1 argument.
How it can be useful?
Imagine we have Map
function defined for slices that expects a 1-argument function f
as a parameter. Map
function applies function f
to each element of the slice and returns a new slice as a result. It could be defined like this (Seq
type is a type based on regular Go slices type Seq[A any] []A
with additional methods defined):
func (seq Seq[A]) Map(f func(A) A) Seq[A] {
r := EmptySeq[A](seq.Length())
for _, e := range seq {
r = r.Append(f(e))
}
return r
}
And then we define a simple function Add
to add 2 numbers:
func Add(a int, b int) int {
return a + b
}
And now we want to use Map
operation to transform each element of the slice using Add
function (add number 10 to each element of the slice):
seq := Seq[int]{1, 2, 3, 4, 5, 6, 7}
//Would not work! Map expects a function of 1 argument but Add has 2...
r := seq.Map(Add)
To help with that we can use currying:
r := seq.Map(Curry2(Add)(10))
fmt.Println(r)
//Output: [11 12 13 14 15 16 17]
Curry2
converted the provided function of 2 arguments (Add
) into a chain of 2 functions each having exactly 1 argument. And then we provided 10
value for the first 1-argument function in that chain, which returned us a remaining 1-argument function expecting the second operand for Add
operation. This returned function had only 1 argument, thus it was compatible with Map
function.
How Curry2
function is implemented under the hood? In fact, the implementation is very straightforward:
func Curry2[A, B, C any](f func(A, B) C) func(A) func(B) C {
return func(a A) func(B) C {
return func(b B) C {
return f(a, b)
}
}
}
Curry3
... functions could be implemented similarly for currying functions of 3 or more arguments.
UnCurrying
Since we are able to Curry something we should be able to UnCurry it back via UnCurrying.
UnCurrying is an operation opposite to Currying. It takes a chain of 1-argument functions and coverts it back to 1 function taking multiple arguments.
Usage could look like this:
f := func(a int) func(bool) func(float64) string {
return func(b bool) func(float64) string {
return func(c float64) string {
return fmt.Sprint(a) + " " + fmt.Sprint(b) + " " + fmt.Sprint(c)
}
}
}
unCurriedF := UnCurry3(f)
r := unCurriedF(1, true, 5.5)
fmt.Println(r)
// Output: 1 true 5.5
UnCurry3
function takes a chain of 3 1-argument functions and converts it to 1 function of 3 arguments. It's implemented trivially under the hood:
func UnCurry3[A, B, C, D any](f func(A) func(B) func(C) D) func(A, B, C) D {
return func(a A, b B, c C) D {
return f(a)(b)(c)
}
}
Other UnCurry
functions could be implemented similarly for uncurrying function chains with a different number of functions.
Conclusion
In this article we looked into the concept called Currying (and the opposite concept UnCurrying). It's primarily used in functional-first languages but even in more imperative languages like Go it could sometimes be of service. If you are interested to explore more practical functional programming concepts applicable in Go and/or would like to get hands-on, welcome to join me on GitHub where I develop Go4Fun library for Go: github.com/ialekseev/go4fun. Thanks for reading!