The beauty of currying
Sinuhe Jaime Valencia
Posted on June 8, 2022
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)
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
}
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
)
)
//...
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()
- Using a class:
val imdbService = MovieService(
params = "default",
domain = Domains.IMDB
)
imdbService.getMovie("scott-pilgrim")
- 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)
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"))
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
Is the same as:
postcall :: String -> (Int -> (String -> ([(String, String)] -> a)))
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")]
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))
}
}
}
}
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")({})
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...
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());
}
}
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
Posted on June 8, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.