Currying in Go

Currying in Go

·

4 min read

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!