DEV Community

Cover image for Forever Functional: Many flavors of currying
OpenReplay Tech Blog
OpenReplay Tech Blog

Posted on • Updated on • Originally published at blog.openreplay.com

Forever Functional: Many flavors of currying

by Federico Kereki

In Functional Programming, an important theoretical method is called "currying" -- but in practice it's an useful concept too, so we'll devote this article to explaining what currying means, how we may use it, and many ways of implementing it (which will serve to look at not often used JavaScript functions).

What is currying, and why should we care?

Currying is a technique that lets you work with unary functions -that is, functions with just a single parameter- even if you need functions with multiple parameters. Why would we need this? The method has do with theoretical considerations, because it allows us to work with a single type of functions, making logical analysis rather easier. This was invented back in the 19th Century, and later worked on by Moses Schönfinkel, a Russian logician, but the technique gets its name from Haskell Curry. (Yes, the same person whose name is a functional programming language!)

To make the process clearer -and we'll to actual code soon- currying is a process in which we transform a function with multiple arguments into a series of nested functions, each of which gets a single argument and produces the next one. The following diagram (from page 187 of my MASTERING JAVASCRIPT FUNCTIONAL PROGRAMMING book) shows the concept.=

Currying explained with a diagram

Suppose we have a f(a,b,c) function with three parameters. In normal programming, we invoke it with three arguments, and we get a result, as shown at the top of the figure. However, we could have a curried version of the function, instead. If so, we would call it with a single argument, a, and it would return not a value, but another function. We would call this new function with a single argument too, b in this case, and it would result yet another function. When we call this third function with an argument, c, then we get the result we wanted. In terms of pure JavaScript, we would call the first function as f(a,b,c) but we would call the curried equivalent, say curriedf, as curriedf(a)(b)(c).

How can we use this? Let's consider some examples. For an ecommerce app, we could have a function that adds a tax to an amount, as shown below.

const addTax = (taxRate, amount) => {
  return amount * (1 + taxRate / 100);
}
Enter fullscreen mode Exit fullscreen mode

Now, if you want to localize the application for a region in which the standard local tax is 3%, and if you had a curried version of addTax() you could just write

const addLocalTax = curried_version_of_addTax(3);
Enter fullscreen mode Exit fullscreen mode

After this, instead of using everywhere addTax(3,amount) you could just write addLocalTax(amount), and it's clearer.

Let's have another example: imagine you have a function telling if a person is older than a certain age.

const isOlderThan(minAge, personAge) = personAge >= minAge;
Enter fullscreen mode Exit fullscreen mode

If in your country a person is considered an adult at 21 years old, with a curried version of isOlderThan() you could write things like the following.

const isAdult = curried_version_of_isOlderThan(21);

if (isAdult(student.age)) { ... }

const adultAges = agesArray.filter(isAdult);
Enter fullscreen mode Exit fullscreen mode

Reading code that uses isAdult(age) is more understandable than reading isOlderThan(21,age) so I would say that currying gets clearer code. A last example along the same lines is as follows. Logging is very common, and you could have a function like this:

const myLogger = (severity, textToShow) => {
    // produce some logging message, in different styles or colors
    // depending on the value of the severity argument
}
Enter fullscreen mode Exit fullscreen mode

You could certainly write myLogger("WARNING", someText) and myLogger("CRITICAL", someOtherText), but with a curried version of the logger function you could define:

const normalLogger = curried_version_of_myLogger("NORMAL");
const alertLogger = curried_version_of_myLogger("WARNING");
const severeLogger = curried_version_of_myLogger("CRITICAL");

// and then usage could be...

normalLogger("Opening files...")
alertLogger("Space is getting low");
severeLogger("API is not responding");
Enter fullscreen mode Exit fullscreen mode

OK, you get the idea; by currying, you can produce new functions out of old ones, which let you write simpler, easier-to-read code. There's only one problem... how do you get a curried version of a function? Some languages, like Haskell or Scala, provide currying as a standard, but we'll have to work a bit with JavaScript, as we'll see in the following sections.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Currying by hand

Let's start by having a couple nonsensical functions, just to show what currying is and how to do it by hand. Currying applies to functions with more than one parameter, so let's have a binary fn2() function and a ternary fn3() one; they just produce a string including the received arguments.

const fn2 = function (a, b) {
  return `TWO... ${a}/${b}`;
};

const fn3 = function (c, d, e) {
  return `THREE... ${c}-${d}-${e}`;
};
Enter fullscreen mode Exit fullscreen mode

To curry these functions by hand, we write the following.

const fn2H = (a) => (b) => fn2(a, b);

const fn3H = (a) => (b) => (c) => fn3(a, b, c);
Enter fullscreen mode Exit fullscreen mode

The code merits careful study. Our new fn2H() function is now a unary function, that returns a new unary function, that calls the original one. Similarly, fn3H() is an unary function that returns an unary function that returns an unary function that calls the original one; try saying this fast! We can check that everything works as expected.

console.log(fn2(1, 2));  // 1/2
console.log(fn2H(1)(2)); // 1/2

console.log(fn3(2, 3, 4));  // 2-3-4
console.log(fn3H(2)(3)(4)); // 2-3-4
Enter fullscreen mode Exit fullscreen mode

So, currying by hand works, but it's a chore... we can do better by writing a higher order function (please check my previous article on the topic) that will take care of the needed transformations; let's see how!

Currying by binding

There are several ways of doing currying, and I picked using the bind() method. To quote the MDN documentation, this method "creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.". In other words, applying bind() to a function produces a new function that has some arguments already fixed, and "waits" for the rest of them; exactly what we did above by hand!

We can automatically curry a function by using the following curryByBind() function.

const curryByBind = (fn) =>
  fn.length === 0 ? fn() : (p) => curryByBind(fn.bind(null, p));
Enter fullscreen mode Exit fullscreen mode

Let's check if it works, before analyzing it in detail.

const fn2C = curryByBind(fn2);
console.log(fn2(1, 2));  // 1/2
console.log(fn2C(1)(2)); // 1/2

const fn3C = curryByBind(fn3);
console.log(fn3(2, 3, 4));  // 2-3-4
console.log(fn3H(2)(3)(4)); // 2-3-4
Enter fullscreen mode Exit fullscreen mode

How does this binding work? The function.length property indicates how many parameters are expected by a function. When we call curryByBind() with a function, it uses recursion with two distinct cases:

  • if the given function expects no parameters, we can just call it.
  • otherwise, we produce a function that will get just one argument, bind it to the function (which will now expect one fewer parameter), and curries the result

This works! But it's not very flexible... let's enhance it!

Partial application

We managed to curry our functions, so essentially that means that we'll write fn2H(a)(b) instead of fn2(a,b), and fn3H(a)(b)(c) instead of fn3(a,b,c). But wouldn't it be nice if we could also pass all parameters to our curried functions, so fn2H(a,b) and fn3H(a,b,c) would also work? This concept is called partial application. Intuitively, currying takes a function and produces a (also curried) function that takes one fewer parameter than the original one; partial application takes a function and produces another function that takes one or more fewer parameters. This can be hard to understand in this vague way, but let's see what our new functions could do. (And if all of this isn't totally clear, the next article in the series will focus on Partial Application exclusively; wait for it!) All the following should work, assuming we have new curryByBind() function that does partial application instead of currying; we'll get to that after the examples.

console.log(fn2(1, 2));      // 1/2
console.log(fn2C(1)(2));     // 1/2
console.log(fn2C(1, 2));     // 1/2 ** NEW **

console.log(fn3(2, 3, 4));   // 2-3-4
console.log(fn3C(2)(3)(4));  // 2-3-4
console.log(fn3C(2, 3, 4));  // 2-3-4 ** NEW **
console.log(fn3C(2, 3)(4));  // 2-3-4 ** NEW **
console.log(fn3C(2)(3, 4));  // 2-3-4 ** NEW **
Enter fullscreen mode Exit fullscreen mode

As you can see, we now have a lot of flexibility for calling our curried functions; we can provide arguments one at a time (as for the standard currying definition) or we can "mix and match" in any way we please! Getting this to work is easy, and just requires a small change. Compare the new version of curryByBind() below with the previous one; can you spot what's different?

function curryByBind(fn) {
  return fn.length === 0 ? fn() : (...p) => curryByBind(fn.bind(this, ...p));
}
Enter fullscreen mode Exit fullscreen mode

We have used spreading to allow currying to work with more than one fixed argument. Instead of binding just one argument (p) we may bind several (...p) in a single step, and return a new function; easy!

This is all very fine, but we could be bothered by having to curry functions by hand and using a separate function... can we do better? Yes, we can, and we'll examine how now.

Prototype currying

In the examples above, given a function we had to manually create an alternative curried one. We can do without this if we directly modify the prototype of all functions so currying will be implicit. The needed change is the following.

Function.prototype.curry = function (...p) {
  return curryByBind(this)(...p);
};
Enter fullscreen mode Exit fullscreen mode

We have modified the prototype for all function objects, so every function will now enable currying. We can now write code as follows.

console.log(fn2(1, 2));            // 1/2
console.log(fn2.curry(1, 2));      // 1/2 ** NEW **

console.log(fn3(2, 3, 4));         // 2-3-4
console.log(fn3.curry(2, 3, 4));   // 2-3-4
console.log(fn3.curry(2, 3)(4));   // 2-3-4
console.log(fn3.curry(2)(3)(4));   // 2-3-4
console.log(fn3.curry(2)(3, 4));   // 2-3-4
Enter fullscreen mode Exit fullscreen mode

By the way, you could even get really weird and write things like fn2.curry()(1)()(2)) or fn3.curryFn2()(2)()()(3, 4) and everything would still work -- though I have no idea why you would do this! With this change to the function prototype, we now have currying available without having to call any external function. Whenever we want the curried version of a function, we just use fn.curry(...) instead of using fn(...); simple! But this code isn't fully safe; some developer could change the curry function by assigning a new value. We really want to do this:

Object.defineProperty(Function.prototype, "curry", {
  get: function () {
    return curry(this);
  },
});
Enter fullscreen mode Exit fullscreen mode

Using Object.defineProperty() ensures that the newly added property (curry) cannot be modified and won't be enumerated as a property; check the configurable and enumerable options.

With this last version our code, the examples we provided at the beginning would have actually been written as:

const addLocalTax = addTax.curry(3);

const isAdult = isOlderThan.curry(21);

const normalLogger = myLogger.curry("NORMAL");
const alertLogger = myLogger.curry("WARNING");
const severeLogger = myLogger.curry("CRITICAL");
Enter fullscreen mode Exit fullscreen mode

To work with a curried version of some fn(...) function, we just use fn.curry(...) instead; easy!

Summing up

Are we done? Yes, the last version of the code is good, can be applied with no problem, and will help you write clearer, terser code. There's a couple of points we didn't delve on, and that you should take into account:

  • currying works only with functions with a known, fixed number of parameters; it won't work with functions that can receive an undefined number of arguments.
  • currying applies to functions, not methods. If you want to curry methods such as .map() or .filter(), you could always first transform the method into a function (as we saw in a previous article) and then everything would be fine.

Currying is a basic technique in functional programming, and I hope I've been able to show that it's easy adding this tool to your JavaScript arsenal; give it a try!

newsletter

Top comments (0)