Two days ago, I announced a preview release of Underscore that integrates with the new Node.js way of natively supporting ES modules.1 Yesterday, somebody responded on Twitter with the following question:
Can you do Ramda-style data last functions?
He or she was referring to one of the main differences between Underscore and Ramda. In Underscore, functions typically take the data to be operated on as the first parameter, while Ramda takes them as the last parameter:
import _ from 'underscore';
import * as R from 'ramda';
const square = x => x * x;
// Underscore
_.map([1, 2, 3], square); // [1, 4, 9]
// Ramda
R.map(square, [1, 2, 3]); // [1, 4, 9]
The idea behind the data-last order in Ramda is that when doing partial application, the data argument is often supplied last. Taking the data as the last parameter removes the need for a placeholder in such cases:
// Let's create a function that maps `square` over its argument.
// Underscore
const mapSquare = _.partial(_.map, _, square);
// Ramda with explicit partial application
const mapSquare = R.partial(R.map, [square]);
// Ramda, shorter notation through automatic currying
const mapSquare = R.map(square);
// Ramda with currying and placeholder if it were data-first
const mapSquare = R.map(R.__, square)
// Behavior in all cases
mapSquare([1, 2, 3]); // [1, 4, 9]
mapSquare([4, 5, 6]); // [16, 25, 36]
As the example shows, it is especially the curried notation that makes data-last attractive for such scenarios.
Why doesn't Underscore do this? There are several reasons for that, which I put in a footnote.2 Nevertheless, making Underscore behave like Ramda is an interesting exercise in functional programming. In my answer below, I'll show how you can do this in just a few lines of code.
1 At the time of writing, if you want to try it, I recommend installing underscore@preview from NPM. This ensures that you get the latest preview version. I just published a fix that bumped the version to 1.13.0-1. I will release 1.13.0 as underscore@latest some time in the near future.
2 Reasons for Underscore to not implement data-last or currying:
- Underscore was born when Jeremy Ashkenas factored out common patterns from DocumentCloud (together with Backbone). As it happens, neither data-last partial application nor currying were common patterns in that application.
- Changing Underscore from data-first to data-last would break a lot of code.
- It is not a universal rule that data are supplied last in partial application; supplying the data first is equally imaginable. Thus, data-last isn't fundamentally better, it's just making a different tradeoff.
- While currying is nice, it also has some disadvantages: it adds overhead and it fixes the arity of a function (unless you make the function lazy, which adds more overhead). Underscore works more with optional and variadic arguments than Ramda, and also prefers making features that add overhead opt-in instead of enabling them by default.