In mathematics and computer science currying is the technique of translating a function that takes multiple arguments into a sequence of families of functions, each taking a single argument.
The Pipe Analogy
One constraint of FP is that functions should only receive one input, which are technically known as unary functions. We can think of a function as a pipe that takes in one input and produces one output. This is similar to how a water pipe takes in water and produces a flow of water out the other end.
But what if we need to write a function that takes in multiple input values? We could pass all of the inputs as properties of a single object and call it a day. For example:
const greet = ({name: string, greeting: string}) => { return `${greeting}, ${name}!`;};
greet({name: 'John', greeting: 'Good morning'}); // => Good morning, John!
This is a perfectly valid approach, but it does introduce some potential issues that can make functions less predictable and harder to optimize in a purely functional paradigm:
- We introduce potential mutation risks if the object is modified elsewhere in the program.
- We’re requiring the caller to know the exact property names. If the function expects
{ name: string, greeting: string }
but receives{ name: 'Alice' }
, it could break at runtime.
Curried Functions: Building a Pipeline
The FP approach to deal with this is to create an outer function that receives a single input, wrapping an inner function (also unary). For example, let’s rewrite the previous example in a curried way:
function greet(greeting: string) { return function(name: string) { return `${greeting}, ${name}!` }}
const result = greet('Hello')('Bob')
console.log(result) // => Hello, Bob!
Note that greet
is a function that returns another function. The first function takes in a single input, greeting
, and returns another function that takes in a single input, name
. The inner function then produces the final output. For example:
console.log(greet('Hello'))
We’re calling the only the outer function greet
, this is the output:
function (name) { return `${greeting}, ${name}!`;}
Where’s the pipeline?
Let’s rewrite the previous example in a more functional style:
const greet = (greeting: string) => (name: string) => `${greeting}, ${name}!`;
Using arrow functions this resembles a series of pipes that take in one of the inputs and pass the output to the next pipe, creating a sort of pipeline.
This is similar to how a water pipe can be connected to multiple other pipes, each taking in one input and producing one output. Let’s create another simple example:
type Sum = (a: number) => (b: number) => number;const sum: Sum = (a) => (b) => a + b;
const result = sum(1)(2);console.log(result); // => 3
You may be wondering, how this is useful? Well, imagine we want to write a function that increments a number by a certain value. We could write a function reusing our sum
function:
type Increment = (value: number) => number;const increment: Increment = (value: number) => sum(value)(1);
const result = increment(5);console.log(result); // => 6
What about decrementing?
type Decrement = (value: number) => number;const decrement: Decrement = (value: number) => sum(value)(-1);
const result = decrement(5);console.log(result); // => 4
Currying Functions
Now imagine that we have a function that takes in multiple inputs, and we want to curry it so we can use it as a curried function:
// Just an usual binary functionconst normalSum = (a: number, b: number) => a + b
type Curry2 = (f: (a: number, b: number) => number) => (a: number) => (b: number) => numberconst curry2: Curry2 = f => a => b => f(a, b)
const curriedSum = curry2(normalSum)
const partialResult = curriedSum(1)console.log(partialResult) // b => f(a, b)
const finalResult = partialResult(41)console.log(finalResult) // 42
In the code above:
normalSum
is a normal function that takes in two inputs.curry2
is a function that takes in a functionf
and returns a curried version of it.curriedSum
is the curried version ofnormalSum
.partialResult
is the result of callingcurriedSum
with the first input, which returns a function that takes in the second input.finalResult
is the result of callingpartialResult
with the second input, which produces the final output.
So what’s the point of currying?
- It allows us to create partial functions that can be reused in different contexts ().
- It allows us to write multi-argument functions, which is easier to reason, then turn them into their curried versions.
- We can use the partial functions to create other versions of the same function, like
increment
anddecrement
in the previous example. - Some libraries that include multi-argument functions can be curried, and the resulting functions can be used in a more functional way.
Developers often write regular functions first and curry them later if needed. Many libraries, like Lodash, provide a _.curry function so you don’t have to manually curry everything.
A Generic Curry Function
Let’s rewrite curry2
as a generic curry function that can curry any function, regardless of the type of their arguments. Here’s an example:
type Curry2 = <T, U, V>(f: (a: A, b: B) => C) => (a: A) => (b: B) => C
This is a generic function that takes in a function f
that takes in two arguments of type A
and B
, and returns a value of type C
. The curried version of the function takes in the first argument of type A
, and returns a function that takes in the second argument of type B
, and returns a value of type C
. Let’s see how this works in practice:
const normalSum = (a: number, b: number) => a + b
type Curry2 = <TA, B, C>(f: (a: A, b: B) => C) => (a: A) => (b: B) => C
const curry2: Curry2 = f => a => b => f(a, b)
const curriedSum = curry2(normalSum)
const partialResult = curriedSum(1)console.log(partialResult) // b => f(a, b)
const finalResult = partialResult(41)console.log(finalResult) // 42
This works exactly the same as before, but now we can use it with any function that takes in two arguments of any type. For example:
const normalConcat = (a: string, b: string) => a + b
const curriedConcat = curry2(normalConcat)
const partialResult = curriedConcat('Hello')console.log(partialResult) // b => f(a, b)
const finalResult = partialResult('World')console.log(finalResult) // HelloWorld