Article co-authored by: Andrew D'Amelio and Yuri Takhteyev

At rangle.io we've been fans of the functional programming style for a while and have used Underscore and Lodash extensively on many projects. However, recently we started using a new library, Ramda, that on the surface seems very similar to Underscore, but which turns out to be different in a small but significant way. Ramda offers roughly the same set of methods as Underscore, but the way it offers them makes functional composition easy.

The difference between Ramda and Underscore comes down to two core concepts: currying and composition.

Currying

Currying is the process of turning a function that expects multiple parameters into one that, when supplied fewer parameters, returns a new function that awaits the remaining ones.

R.multiply(2, 10); // returns 20

Here we have to pass both parameters, 2 and 10, to call the function.

var multiplyByTwo = R.multiply(2);
multiplyByTwo(10); // returns 20

Pretty neat! We created a new function multiplyByTwo which is just the value 2 baked into multiply(). We can now pass any value to our multiplyByTwo function. The reason we can do this is because all of Ramda's function are curried.

Currying proceeds from right-to-left: when you skip some arguments, Ramda assumes you've skipped the ones on the right. Because of that, Ramda functions that take an array and a function normally expect the function as the first argument and the array as the second. This is the reverse of how Underscore does it:

_.map([1,2,3], _.add(1)) // 2,3,4

Versus:

R.map(R.add(1), [1,2,3]); // 2,3,4

The combination first-operation-then-data with right-to-left currying allows us to just specify what we want to do and get back a function that does that. We can then call that function with the actual data. Currying becomes easy and practical:

var addOneToAll = R.map(R.add(1));
addOneToAll([1,2,3]); // returns 2,3,4

We've now got a function addOneToAll() that we can reuse in a variety of contexts.

Here is a somewhat more complex but also more practical example. Suppose we want to make a request to the server, get an array of items, and then extract the field "cost" from each item. Using Underscore we might do this:

This is because, when we call R.pluck('cost'), it returns a function that extracts the "cost" field from each item in the supplied array - which is precisely the function we want to pass to .then().

To get the full benefit of currying, however, we need to combine it with composition.

Composition

Mathematically speaking, functional composition is an operation that takes functions f and g, returns a function h such that: g(x) = f(g(x)). Ramda offers a compose() function to do this. Compose mixes well with currying, since we can build larger functional behaviors, from smaller functional components.

var getCostWithTax = R.compose(
R.multiply(1 + TAX_RATE), // calculate the tax
R.prop('cost') // pull out the 'cost' property
);

This gives us a function that gets the property "cost" out of an object and then multiplies the result by 1.13.

The standard "compose" function is right-associative. That is, operations proceed from right to left. If you find this unintuitive, you can use R.pipe(), which is the same as R.compose() but works from left to right:

var getCostWithTax = R.pipe(
R.prop('cost'), // pull out the 'cost' property
R.multiply(1 + TAX_RATE) // calculate the tax
);

You are not limited to composing just two functions. R.compose and R.pipe can take up to 10 arguments.

It's worth noting that currying and composing are supported by libraries such as Underscore too. They are rarely used, however, since the data-first order of parameters makes currying Underscore's methods impractical. Ramda makes it easy to apply currying and composition in practice.

At first, we quickly fell in love with Ramda. Ramda's style leads to code that's extendable, composable, testable, and declarative. Composing functions feels very natural, and results in JavaScript that's just easier to understand.

then...

As we started using Ramda more and more we discovered that things can get a bit messier when you have asynchronous functions returning promises:

var getCostWithTaxAsync = function() {
var getCostWithTax = R.pipe(
R.prop('cost'), // pull out the 'cost' property
R.multiply(1 + TAX_RATE) // multiply it by 1.13
);
return getItem()
.then(getCostWithTax);
}

While this is cleaner than it would have been without Ramda, what we wished we could do was just this:

var getCostWithTaxAsync = R.pipe(
getItem, // get the item
R.prop('cost'), // pull out the 'cost' property
R.multiply(1 + TAX_RATE) // multiply it by 1.13
);

The reason we couldn't do this is because getItem() returns a promise, while the function returned by R.prop() expects to be called with the actual value.

Promise-Aware Composition

We got in touch with Ramda contributors and proposed a version of compose that would automatically unwrap promises, so that asynchronous functions could be composed together with functions that expect actual results. After a long discussion, we settled on implementing these as new functions: R.pCompose() and R.pPipe() - with "p" standing for "promise".

With R.pPipe we could do exactly what we wanted to do:

var getCostWithTaxAsync = R.pPipe(
getItem, // get a promise for the item
R.prop('cost'), // pull out the 'cost' property
R.multiply(1 + TAX_RATE) // multiply it by 1.13
); // returns a promise for the cost with tax

Since our code tends to leverage promises quite heavily, we expect R.pPipe() to do quite a lot of heavy lifting in our future code. Give it a try and let us know what you think!

I agree to receive email communications from Progress Software or its Partners, containing information about Progress Software’s products. Consent may be withdrawn at any time.

We see that you have already chosen to receive marketing materials from us. If you wish to change this at any time you may do so by clicking here.

Thank you for your continued interest in Progress. Based on either your previous activity on our websites or our ongoing relationship, we will keep you updated on our products, solutions, services, company news and events. If you decide that you want to be removed from our mailing lists at any time, you can change your contact preferences by clicking here.