NML

Practical currying in Javascript

By Charl Marais

I came across currying years ago when I first tried my hand at learning Haskell. Although I understood it, I could not fathom how it could be useful in mainstream imperative languages. This is the case no more and I hope I can help to shine some light on currying and how it can help you (at least when using Javascript).

Curry whaaaaatt?

Let me first just define what currying is. From the Wikipedia article on currying:

currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument

In simpler(ish) terms, currying just means you take a method that receives more than one argument and swap that out for a chain of method calls the each takes one argument. Look at the implementation below (gist):

function sum(a, b) {
  return a + b;
}
console.log(sum(3, 5)); // prints 8

The currying version of this is (gist):

function sum(a) {
  return b => a + b;
}
console.log(sum(3)(5)); //prints 8

“Charl!” I hear you shout. “That’s just stupid! Why on earth would you do that?!” Hold on, hold on. Remember, I am still just defining it. Give me a little bit more of your time!

To explain the curry example above using English:

sum takes an argument and returns a function takes a second argument and adds it to the argument received in the first.

We are leveraging Javascript closures here. A more verbose version of the currying sum above is (gist):

function sum(a) {
  function sumAWith(b) {
    return a + b;
  }
  return sumAWith;
}
console.log(sum(3)(5)); //prints 8

But, but… how on earth is that useful?

Let me show you now how currying can be used in real code to give you real benefit.

Let us build an advanced filter function called advancedFilter (I know, original, huh?). The function will take two arguments. The first argument, collection, will be an array containing objects. The second argument, match, will be just an object. The function must filter out all objects in collection that do not have matching properties (and their associated values) in match.

So if we want to filter a collection like [ { name: "apple", color: "red" }, { name: "pear", color: "yellow" } ] and we are interested in only red fruit. We would call our method with the aforementioned fruit collection and [ { color: "red } ] as the second argument. advancedFilter should then match only the apple, and return that in an array.

The following code is a reasonable expectation of a solution you can expect to see to the problem above (gist).

function advancedFilter(collection, match) {
  const keysOf = obj => Object.keys(obj)
 
  const toKeyValue = (obj, key) => ({key, value: obj[key]});
 
  const matchKeyValue = (keyValue1, keyValue2) =>
      keyValue1.key === keyValue2.key &&
      keyValue1.value === keyValue2.value;

  const matchOnAll = (keyValues, keyValue) => 
      keyValues.some(item => matchKeyValue(keyValue, item))

  // end functions
 
  const matchKeyValues = keysOf(match).map(item => toKeyValue(match, item));

  return collection.reduce((acc, cur) => {
    const curKeyValues = keysOf(cur).map(item => toKeyValue(cur, item));
    return matchKeyValues.every(item => matchOnAll(curKeyValues, item))
          ? [...acc, cur]
          : acc
  }, []);
}

You can test out if this works on codepen.io.

The algorithm first maps the match argument to an array of key-value pairs, matchKeyValues. Then we reduce over collection by:

  1. Mapping each object cur to key-value pairs, curKeyValues.
  2. Adding cur to the returned array only if every key-value pair in matchKeyValues exists in curKeyValues.

The next code block is the same function but changed subtly to leverage currying to make the algorithm more readable (gist).

function advancedFilter(collection, match) {
  const keysOf = obj => Object.keys(obj)
  
  const toKeyValue = obj => key => ({key, value: obj[key]});

  const matchKeyValue = keyValue1 => keyValue2 =>
      keyValue1.key === keyValue2.key &&
      keyValue1.value === keyValue2.value;

  const matchOnAll = keyValues => keyValue => 
      keyValues.some(matchKeyValue(keyValue))

  // end functions

  const matchKeyValues = keysOf(match).map(toKeyValue(match));

  return collection.reduce((acc, cur) => {
    const curKeyValues = keysOf(cur).map(toKeyValue(cur));
    return matchKeyValues.every(matchOnAll(curKeyValues))
          ? [...acc, cur]
          : acc
  }, []);
}

You can test out if this works on codepen.io.

The utility functions have changed slightly to accept a single argument and return a function that matches the expected function signature of array functions like every, some and filter. It is the immediately evaluated and the return function passed on to the array function. Javascript closures allow the array function to call the provided callback and still have access to the first argument used (or how many ever was in the call chain).

Readability

In the second version, the algorithm has suddenly become more readable. We did this by taking away parameter noise on the array functions we’re using. Here are some side by side comparisons:

matchKeyValue

matchOnAll

toKeyValue

The statements to my eyes now read more naturally. For example:

every matchKeyValues should matchOnAll curKeyValues

Abstraction and reusability

Other than the readability benefit, you also increase the abstraction and re-usability of your functions. Of course, that is only true if the local functions here were extracted and imported for use. The reason why your re-useability goes up is that you can chain these abstractions in many different novel ways. With multi-argument functions, you are generally going be restricted to more specific use cases.

Conclusion

I am by no means encouraging you to go and slash all arguments but one from your functions. Far from it. The use case here is very much aimed at reducing the complexity contained in array function callbacks, and it is an easy and natural fit.

Be on the lookout for where the use of currying can improve your code quality, and I hope this article helps you in that endeavour just a little.