DEV Community

Sinuhe Jaime Valencia
Sinuhe Jaime Valencia

Posted on

The beauty of currying

What is currying?

Currying is the fine art of transforming a function with arity n into n functions with arity 1.

This means: Given a function that takes X parameters, generate X functions that take only 1 parameter.

Using the wikipedia example:

  • Given x = ƒ(a, b, c) it becomes:
    • h = g(a)
    • i = h(b)
    • x = i(c)
  • Or in a single sequence call: x = g(a)(b)(c)

The name comes from Haskell Curry, a famous mathematician who developed a lot of concepts in modern math.

What it means in modern programming?

It simply means that you have a way to reduce the complexity of a function call creating intermediate functions which in turn return newer functions.
It will be more clear with examples...

A taste of currying

Let's start with a "simple function" it takes 2 numbers and returns the sum of them:

sum = { a: Int, b: Int } => a + b

Let's imagine for a second that we have a curry function that will give back the curried version of our function, a call of: curry(sum) will generate curriedSum which can be invoked like: curriedSum(a)(b) instead of the original call: sum(a, b).

There's also the possibility of actually set some of its values and do PARTIAL APPLICATION which means setting a value for one (or more) of the parameters.

On our sum example:

val curriedSum = curry(sum) // return value is: { a -> { b -> a + b } }
// Or also expressed as: (Int) -> (Int) -> Int
val add5 = curriedSum(5) // returns: (Int) -> Int = { b -> 5 + b } 
val add7 = curriedSum(7) // returns: (Int) -> Int = { b -> 7 + b } 

add5(42) // 47
add7(13) // 20
add5(add7(10))
curriedSum(17)(7)// 24 :: add5(add7(10)) -> add5(17)
Enter fullscreen mode Exit fullscreen mode

Why would I want that?

There are multiple reasons for wanting to curry a function:

  • We don't have all the values that will be passed right now, we usually have to create callbacks or proxies or mechanism to obtain these values before actually calling a function.
  • We need to pass a function as parameter to another function (callbacks) and we already have a function defined to do the work.
  • We want to partially apply a function to pass it along to other places but keeping our data contextualized in there, sounds weird but imagine you have to pass a callback or pass a filtering function, and you already have a function that does the job, but with additional flags or parameters.
  • We need a simple way to provide a complex API shared between parts at different moments.

Truth is you maybe don't need it or have other approaches that can do the work as well, still is worth the shot to understand how it works in case you need it some day.

Let's see it in action

For this example, let's imagine we have a function that can do a POST to a web service and returns information.

fun <T> postCall(
    domain: String, 
    port: Int, 
    path: String, 
    queryParams: QueryParams,
): T {
    //... Here happens the magic call
}
Enter fullscreen mode Exit fullscreen mode

Each call will be complex:

postCall<MovieResponse>(
  "moviedb.com", 
  8090, 
  "/movies/scott-pilgrim", 
  QueryParams(
    "order" to "asc",
    "type" to Types.JSON,
    "comments" to false
  )
)
//...
postCall<MovieResponse>(
  "moviedb.com", 
  8090,
  "/movies/lego-movie", 
  QueryParams(
    "order" to "desc",
    "type" to Types.JSON,
    "comments" to true
  )
)
//...
Enter fullscreen mode Exit fullscreen mode

This is error-prone and can be improved:

  • Using a builder:
  createMovieCall()
    .forMovie("lego-movie")
    .withParams("order" to "desc", "type" to Types.JSON, "comments" to true)
    .call()
Enter fullscreen mode Exit fullscreen mode
  • Using a class:
    val imdbService = MovieService(
        params = "default", 
        domain = Domains.IMDB
    )
    imdbService.getMovie("scott-pilgrim")
Enter fullscreen mode Exit fullscreen mode
  • Other cooler ways that are not as cool as currying.

But with currying we can do something like:

val movieService = curry(postCall)("moviedb.com")(8090)

val scottPilgrimCall = movieService("/movies/scott-pilgrim")
val scottPilgrimWithComments = scottPilgrimCall("comments" to true)
val scottPilgrimNoComments = scottPilgrimCall("comments" to false)

val legoMovieCall = movieService("/movies/lego-movie")
val legoMovieYAML = legoMovieCall("type" to Types.YAML)
val legoMovieXML = legoMovieCall("type" to Types.XML )
val legoMovieJSON = legoMovieCall("type" to Types.JSON)
Enter fullscreen mode Exit fullscreen mode

This approach allow to create the intermediate calls and keep previous parameters without problems, we can even create a
"movie service generator":

fun movieServiceGenerator(
  movieWeb: string
): (String) -> (QueryParams) -> MovieResponse =
  curry(postCall)(movieWeb)(8090) // Assuming both sites use this port

val imdbService = movieServiceGenerator("imdb.com")
val cuevanaService = movieServiceGenerator("cuevana3.me")

// And the calls will be similar:

val jumanjiIMDB = imdbService("/movie/jumanji")(QueryParams())
val jumanjiCuevana = cuevanaService("/92345/jumanji")(QueryParams("server" to "webfree2"))
Enter fullscreen mode Exit fullscreen mode

The syntax is different but the way we pass data evolves to return functions with partially applied data so, we can build functions with simpler calls.

How to implement it

Depending on what your language allows with functions it can be easy or hard or even unreadable (but easy to use).

For example, in Haskell all calls with multiple parameters are auto curried which means that a function:

postcall :: String -> Int -> String -> [(String, String)] -> a
Enter fullscreen mode Exit fullscreen mode

Is the same as:

postcall :: String -> (Int -> (String -> ([(String, String)] -> a)))
Enter fullscreen mode Exit fullscreen mode

So we can do partial application if needed:

imdbservice = postcall "imdb.com" 8090

scott_pilgrim = imdbservice "/movies/scott-pilgrim"
scott_pilgrim_comments = scott_pilgrim [("comments", "true")]
Enter fullscreen mode Exit fullscreen mode

In JS defining a currying function gets interesting:

function curry(func) {
    return function curried(...args) {
        if (args.length >= func.length) {
            return func.apply(this, args)
        } else {
            return function (...args2) {
                return curried.apply(this, args.concat(args2))
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

But there are a lot of solutions out there, like lodash:

const curried = _.curry(postcall)
const imdbService = curried("imdb.com")(8090)
const legoMovie = imdbService("/movies/lego-movie")({})
Enter fullscreen mode Exit fullscreen mode

Other languages can get complicated as we need to know the number of arguments for our application, like kotlin:

fun <A, B, R> curry(f: (A, B) -> R): (A) -> (B) -> R {
    return { a: A -> { b: B -> f(a, b) } }
}

fun <A, B, C, D, R> curry(f: (A, B, C, D) -> R): (A) -> (B) -> (C) -> (D) -> R =
    { a: A ->
        { b: B ->
            { c: C ->
                { d: D -> f(a, b, c, d) }
            }
        }
    }

fun postCall(
  web: String, 
  port: Int, 
  path: String, 
  params: Map<String, String>): MovieResponse {
    //...
}

val curried = curry(::postCall)
println(curried("imdb")(9090)("/lego")(mapOf("format" to "json")))
// And so on...
Enter fullscreen mode Exit fullscreen mode

Depending on how much your platform (language) allows, it can be messy...


@FunctionalInterface
interface TetraFunction<A, B, C, D, R> {
    R apply(A a, B b, C c, D d);
}

class Curry {
    public static <A, B, R> Function<A, Function<B, R>> curry(BiFunction<A, B, R> f) {
        return (a) -> (b) -> f.apply(a, b);
    }

    public static <A, B, C, D, R> 
    Function<A, 
            Function<B, 
                    Function<C, 
                            Function<D, R>>>> curry(TetraFunction<A, B, C, D, R> f) {
        return (a) -> (b) -> (c) -> (d) -> f.apply(a, b, c, d);
    }
}

//Usage:
class Example {
    static <T> T postCall(
            String movieWeb, 
            int port, 
            String path, 
            Map<String, String> params) {

    }

    public static void main(String[] args){
        Function<String, 
                Function<Integer, 
                        Function<String, 
                                Function<Map<String, String>, 
                                        MovieResponse>>>> curriedPostCall = Curry.curry(Example::postCall);

        MovieResponse movieResponse = curriedPostCall.apply("imdb.com").apply(9890).apply("/lego-movie").apply(new HashMap());
    }
}
Enter fullscreen mode Exit fullscreen mode

The concept and application stays almost the same, only adapting it to each language/platform.

And I did not show this time a complete example as we already covered a lot, but the ideal scenario for using currying is when you have functions taking functions and returning other functions.

Conclusions

  • Functional Programming creates a lot of intermediate data
  • Currying is cool if you need to simplify your function calls
  • Currying is cool if you need to partially apply your functions
  • Currying is not a silver bullet but can help you create a simpler API

Top comments (0)