DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Currying Inside JavaScript
jsmanifest
jsmanifest

Posted on • Originally published at jsmanifest.com

Currying Inside JavaScript

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}`
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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"
*/
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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)
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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))
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

...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])
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Did the caller pass in enough arguments to satisfy all the arguments of fn?
  2. 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)
Enter fullscreen mode Exit fullscreen mode

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

Top comments (4)

Collapse
 
puiutucutu profile image
puiu • Edited on

Nice function - one thing I noticed is that calling the curry function's callback with all the arguments provided at once or one at a time, you get back the same result.

For example:

const adder = (x, y) => x + y;
const curriedAdder = curry (adder);

const a = curriedAdder (1) (2); //=> 3
const b = curriedAdder (1, 2);  //=> 3
Enter fullscreen mode Exit fullscreen mode

I would also recommend throwing an error when there is a mismatch between the number of supplied args and the number of args the function actually needs.

For example, the code below will throw a different error when the curried function is partially applied versus when then curried function is called with all arguments at once.

const isEveryArgumentProvided = x => y => x >= y;

function curry(f) 
{
  function curried(...initialArgs) {
    if (initialArgs.length > f.length) {
      throw new Error(
        `Function \`${f.name}\` supplied ${initialArgs.length} args when expecting ${f.length} args`
      );
    }

    return isEveryArgumentProvided (initialArgs.length) (f.length)
      ? f.apply(this, initialArgs) // received all args for f
      : (...remainingArgs) => curried.apply(this, [...initialArgs, ...remainingArgs])  // more args needed for f
    ;
  }

  return curried;
}

const adder = (x, y) => x + y;
const curriedAdder = curry(adder);

curriedAdder (1) (2) (3); //=> Error: curriedAdder(...)(...) is not a function
curriedAdder (1, 2, 3); //=> Error: Function `adder` supplied 3 args when expecting 2 args
Enter fullscreen mode Exit fullscreen mode
Collapse
 
codercatdev profile image
Alex Patterson

Great article! Why are arrow functions still so much easier on my eyes? πŸ¦„

Collapse
 
bootcode profile image
Robin Palotai

I wonder if one could use bind or does it have drawbacks?

Collapse
 
sesay profile image
sesay

nice...

Classic DEV Post:

Visualizing Promises and Async/Await 🀯

async await