Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
Currying
Named after Haskell Brooks Curry, currying is the process of breaking down a function into a series of functions that each take a single argument.
Currying example
Here’s a simple example. We’ll write a function sum3 takes three numbers and returns their sum.
function sum3(x, y, z) { return x + y + z;}
console.log(sum3(1, 2, 3) // 6
The curried version of sum3 behaves a little differently. For starters, it accepts only one argument and returns a function. The returned function also accepts one argument and also returns a function.
This cycle keeps going until the returned function accepts the last argument. This function, the last one in the chain, finally returns the sum.
function sum3(x) { return (y) => { return (z) => { return x + y + z; }; };}console.log(sum3(1)(2)(3)) // 6
This works because JS supports closures. What’s a closure?
A closure is the combination of a function and the lexical environment within which that function was declared. — MDN
Observe that the last function in the chain only accepts z, but operates on other variables. For all it cares, they are global variables. When in reality, they’re just local variables scoped in different functions.
let x = ...;let y = ...;
return function(z) { return x + y + z;};
Generalised curry
Writing one curried function is all fine and dandy, but this can become a real chore when writing multiple functions. We need a more general approach.
In most functional languages, like haskell, all we have to do is define the function and it gets curried automagically.
let sum3 x y z = x + y + z
sum3 1 2 3-- 6
:t sum3 -- print the type of sum3()-- sum3 :: Int -> Int -> Int -> Int
sum3 :: Int -> Int -> Int -> Int -- function namesum3 :: Int -> Int -> Int -> Int -- curried function definitionsum3 :: Int -> Int -> Int -> Int -- final return
We can’t rewrite the JS engine to curry-ify all functions, but we can come up with a strategy to do so.
The Strategy
Examining the two forms of sum3 reveals an important fact. The actual function body from the plain function is moved as-is into the last function in the chain. Before we reach the last level, we won’t have all the arguments in the execution scope.
function sum3(x, y, z) {//...}
function sum3(x) { return (y) => { return (z) => {// ... }; };}
This means that we can create a wrapper function that collects the arguments and passes them to the real function. All the intermediate nested functions are called accumulator functions — at least that’s what I call them.
function _sum3(x, y, z) { return x + y + z;}
function sum3(x) { return (y) => { return (z) => {return _sum3(x, y, z); }; };}
sum3(1)(2)(3) // 6 <-- It works!
Curry Wrapper
Since we’re using a wrapper function in place of the real function, we can create another function that generates this wrapper function. We’ll call this new function curry — a higher-order function that returns a series of nested accumulator functions that invoke the callback function fn in the end.
function curry(fn) { return (x) => { return (y) => { return (z) => { return fn(x, y, z); }; }; };}
const sum3 = curry((x, y, z) => { return x + y + z;});
sum3(1)(2)(3) // 6 <-- It works!
All we now need are different arity curry functions that take: 0 arguments, 1 argument, 2 arguments and so on …
Recursive Curry
Not really. Instead of writing multiple wrapper functions, we will write a single curry function that works for functions of all arity.
Let’s say we do write multiple curry functions. This is what they’ll look like:
function curry0(fn) { return fn();}
function curry1(fn) { return (a1) => { return fn(a1); };}
function curry2(fn) { return (a1) => { return (a2) => { return fn(a1, a2); }; };}
function curry3(fn) { return (a1) => { return (a2) => { return (a3) => { return fn(a1, a2, a3); }; }; };}
...
function curryN(fn){ return (a1) => { return (a2) => { ... return (aN) => { // N-th nested function return fn(a1, a2, ... aN); }; }; };}
We can make a few observations:
- The i-th accumulator returns another function that is the (i+1)-th accumulator. We can also say that it is the j-th accumulator.
- The i-th accumulator accepts the i-th argument, while keeping the previous i - 1 arguments in its surrounding closure.
- There will be N nested functions, where N is the arity of function fn.
- The N-th function always invokes fn.
Using the above observations, we can see that the curry function returns a nested structure of self-similar accumulator functions. We can easily generate such a structure using recursion.
function nest(fn) { return (x) => { // accumulator function
return nest(fn); };}
function curry(fn) {return nest(fn);}
We obviously need a terminal case else our code will form an infinite fractal. We will store the current nesting depth in a variable i. The terminal case will be when i === N.
function nest(fn, i) { return (x) => { if (i === fn.length) { return fn(...); }
return nest(fn, i + 1); };}
function curry(fn) { return nest(fn, 1);}
Next up, we need to store the accumulated arguments and pass them to fn() in the terminal case. The easiest solution is to create an array args inside curry and pass it to nest.
function nest(fn, i, args) { return (x) => { args.push(x);
if (i === fn.length) { return fn(...args); }
return nest(fn, i + 1, args); };}
function curry(fn) {const args = []; return nest(fn, 1, args);}
Just add a guard for 0-arity functions and we are done.
function curry(fn) { if (fn.length === 0) { return fn; }
const args = []; return nest(fn, 1, args);}
It’s a good idea to test our code at this stage:
const log1 = curry((x) => console.log(x));log1(10); // 10
const log2 = curry((x, y) => console.log(x, y));log2(10)(20); // 10 20
You can run your tests here on codepen.
Tweaks and Optimisations
For starters, we can move nest inside curry, thus reducing the number of arguments passed to nest by reading fn and args from the closure.
function curry(fn) { if (fn.length === 0) { return fn; }
const args = [];
function nest(i) { return (x) => { args.push(x);
if (i === fn.length) { return fn(...args); }
return nest(i + 1); }; }
return nest(1);}
Let’s tweak this new curry to be more functional and not rely on closure variables. We do this by supplying args and fn.length to nest as arguments. Further still, instead of passing the target depth (fn.length) for comparison, we can pass the remaining depth of recursion.
function curry(fn) { if (fn.length === 0) { return fn; }
function nest(N, args) { return (x) => { if (N - 1 === 0) {return fn(...args, x); }
return nest(N - 1, [...args, x]); }; }
return nest(fn.length, []);}
Variadic Curry
Let’s compare sum3 to its older brother sum5:
const sum3 = curry((x, y, z) => { return x + y + z;});
const sum5 = curry((a, b, c, d, e) => { return a + b + c + d + e;});
sum3(1)(2)(3) // 6 <-- It works!sum5(1)(2)(3)(4)(5) // 15 <-- It works!
It works, no doubt. But look at this abomination of a code!
In haskell and other functional languages, the syntax is pretty relaxed. Compared to this monstrosity, let’s see how haskell deals with it:
let sum3 x y z = x + y + zlet sum5 a b c d e = a + b + c + d + e
sum3 1 2 3> 6
sum5 1 2 3 4 5> 15
sum5 1 2 3 (sum3 1 2 3) 5> 17
If you ask me, even the simple function calls in JS look better:
sum5(1, 2, 3, 4, 5) // 15
But this doesn’t mean we have to give up on currying. What we can do is settle for some middle ground. A system where the curried, the uncurried and the syntaxes in between all work.
sum3(1, 2, 3) // plainsum3(1, 2)(3)sum3(1)(2, 3)sum3(1)(2)(3) // curried
We need a simple modification — convert accumulators to variadic functions.
When the i-th accumulator accepts k arguments, the next accumulator won’t be of N - 1 depth, but rather N - k depth. We were using N - 1 because all accumulators were accepting one argument each. This also means that we no longer require the 0-arity guard (why?).
Since we’re now collecting multiple arguments at each level, we need to check if and when the count of arguments crosses arity fn and then invoke it.
function curry(fn) { function nest(N, args) { return (...xs) => { if (N - xs.length <= 0) { return fn(...args, ...xs); }
return nest(N - xs.length, [...args, ...xs]); }; }
return nest(fn.length, []);}
It’s testing time. You can run your tests here on codepen.
function curry(){...}
const sum3 = curry((x, y, z) => x + y + z);
console.log( sum3(1, 2, 3), sum3(1, 2)(3), sum3(1)(2, 3), sum3(1)(2)(3),);// 6 6 6 6
Bonus
We did it! We created a curry function that takes multi-arity functions and returns variadic curried function(s). There is just one more method of currying in JS that I want to show.
In JS, we can bind arguments to a function and create a bound copy. The returned function is “partially applied” — partially, because the function already holds some of its arguments, but requires some more before invocation.
Up until now, curry would return a function that accumulates arguments until all arguments were received and then invoke fn with the arguments. By binding arguments to the function, we can remove the need for multiple nested accumulator functions.
And this is what we get:
function curry(fn) { return (...xs) => { if (xs.length >= fn.length) { return fn(...xs); }
return curry(fn.bind(null, ...xs)); };}
Here’s how it works. curry takes an N-arity function and returns an accumulator function. When the accumulator is invoked with k number of arguments, we check if k >= N, i.e. whether or not the function’s arity is satisfied.
If it is, we invoke fn with the arguments. If not, we create a copy of fn that has the first k arguments bound (partially applied) and pass it to curry as the next fn, with its reduced arity of N - k.
The End
We covered the generic approach to currying by using nested accumulators. This approach will work in any language that supports first-class functions. We saw the difference between strict currying and variadic currying. We also saw how easy things got in JS, thanks to the bind method.
If you’re interested in the source code, you’d have to head over to codepen.
Currying in JS was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.