Currying Inside JavaScript
jsmanifest
Posted on September 10, 2019
Find me on medium
Currying is an advanced technique when working with functions and it's used in multiple programming languages.
When you break down a function that takes multiple arguments into a series of nesting functions, you have a curry. Each nesting function will be expecting to have the next argument(s) to the function.
The curry function will always be returning a new function each time until all of the arguments were received for each invocation. These arguments are able to live throughout the lifetime of the currying through closure and will all be used to execute the final function.
A very basic example can look something like this:
function combineWords(word) {
return function(anotherWord) {
return function(andAnotherWord) {
return `${word} ${anotherWord} ${andAnotherWord}`
}
}
}
To use this, you can invoke the function a number of times until it reaches the last function:
const result = combineWords('hello,')('good')('morning')
console.log(result)
// result: 'hello, good morning'
So what's happening is that combineWords
is a curried function (obviously) and waits for a word to be given before it executes the next function in the series. You can bind 'wow!'
to combineWords
to a variable and re-use it to create other greetings that start with 'wow!'
:
let greet = combineWords('wow!')
greet = greet('nice')
console.log(greet('jacket'))
console.log(greet('shoes'))
console.log(greet('eyes'))
console.log(greet('socks'))
console.log(greet('hat'))
console.log(greet('glasses'))
console.log(greet('finger nails'))
console.log(greet('PS3'))
console.log(greet('pet'))
/*
result:
"wow! nice jacket"
"wow! nice shoes"
"wow! nice eyes"
"wow! nice socks"
"wow! nice hat"
"wow! nice glasses"
"wow! nice finger nails"
"wow! nice PS3"
"wow! nice pet"
*/
If the concept is a little hard to understand, try reading it this way:
The mother is expecting all 4 eggs (arguments) before cooking and her 4 children will each carry one to her, one at a time.
function Egg() {...}
// the curry func
function prepareCooking(cook) {
return function(egg1) {
return function(egg2) {
return function(egg3) {
return function(egg4) {
return cook(egg1, egg2, egg3, egg4)
}
}
}
}
}
const cook = function(...eggs) {
api.turnOnStove()
api.putEggsOnTop(...eggs)
api.pourSalt()
api.serve()
console.log('served children')
return 'served'
}
const start = prepareCooking(cook)
let collect = start(new Egg())
collect = collect(new Egg())
collect = collect(new Egg())
collect = collect(new Egg()) // this steps into the last function witih argument "egg4" which will invoke the callback passed to "prepareCooking"
// result: console.log --> "served children"
// collect === 'served'
In order for the cook
callback to be invoked, all of the 4 eggs needed to be passed in one after the other, each prefilling the next function awaiting for invocation.
If you were to stop at the third egg:
let collect = start(new Egg())
collect = collect(new Egg())
collect = collect(new Egg())
Then since the last function expecting egg4
has not been reached yet, the value of collect
is that function:
function prepareCooking(cook) {
return function(egg1) {
return function(egg2) {
return function(egg3) {
// HERE
return function(egg4) {
return cook(egg1, egg2, egg3, egg4)
}
}
}
}
}
To finish the curry, collect the last egg:
let collect = start(new Egg())
collect = collect(new Egg())
collect = collect(new Egg())
collect = collect(new Egg())
// collect === 'served'
Now it's important to know that each nesting function has all access of the outer scope within the curry function. Knowing this, you can provide custom logic inbetween each nested function to tailor for specific situations. But it's best to leave a curry as a curry and nothing else.
A more advanced curry function can look as follows: (i'm going to provide an ES5
version as well as an ES6
because there are plenty of old tutorials that show ES5 syntax, which might be a little hard to read for newer JavaScript developers)
ES5
function curry(fn) {
return function curried() {
const args = Array.prototype.slice.call(arguments)
const done = args.length >= fn.length
if (done) {
return fn.apply(this, args)
} else {
return function() {
const args2 = Array.prototype.slice.call(arguments)
return curried.apply(this, args.concat(args2))
}
}
}
}
...is the same as:
ES6
const curry = (fn) => {
return function curried(...args) {
const done = args.length >= fn.length
if (done) {
return fn.apply(this, args)
} else {
return (...args2) => curried.apply(this, [...args, ...args2])
}
}
}
Let's explain this example more in detail:
When you call curry(fn)
it will return the inner curried
function that will wait for the next arguments upon invocation. Now when you call this inner function, it evaluates two conditions:
- Did the caller pass in enough arguments to satisfy all the arguments of
fn
? - Or are there still arguments missing that
fn
needs?
If number 1 is the case, then we have all the arguments we need that fn
declared and the curry will end by returning the invocation of fn
and passing all the arguments received to it (basically invoking fn
normally now)
However, if number 2 is the case, then the curry must continue going and we must somehow go back to the inner curried
function so that we can continue to receive more arguments until it satisfies the arguments of fn
. The code return (...args2) => curried.apply(this, [...args, ...args2])
accumulates all the arguments exposed so far and uses them to continue the curry in this case.
There's one important rule:
The function that is to be invoked before waiting for all the arguments to be collected must have a fixed number of arguments. This means that the function cannot have parameters spreaded (ex:
fn(...args)
)
ex:
const curry = (fn) => {
return function curried(...args) {
const done = args.length >= fn.length
if (done) {
return fn.apply(this, args)
} else {
return (...args2) => curried.apply(this, [...args, ...args2])
}
}
}
// This is invalid because it uses ...args. The curry does not understand where to stop
function func(...args) {
//
}
const currying = curry(func)
Conclusion
I think currying is an interesting technique because creating a curry involves composing other advanced techniques. There are closures involved, higher order functions, and recursion.
And that concludes the end of this post. I hope you found something valuable and look out for more in the future!
Find me on medium
Posted on September 10, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024