Practical ramda - functional programming examples

Ramda is a second-generation JavaScript library for functional programming. We’re using it in Mapbox Studio to great effect, and given that functional programming libraries are often documented with mathematical or abstract examples, I think it would be useful to give some examples of how.

Second generation

So Ramda is a second generation functional programming library.

Arguably the first generation was underscore and lodash. In the era when Internet Explorer 8 was still a consideration in JavaScript programming, underscore had a clear purpose: to make JavaScript’s functional methods cross-browser compatible. JavaScript has great, built-in functionality for functional iteration, mapping, and reduction, but these methods, like Array.map and Array.reduce, have been slow to arrive in Internet Explorer. underscore gave the world reliable _.map and _.reduce methods - along with many others - that worked the same in every browser.

In the time since, Internet Explorer 8 has faded into the past and native ES5 methods are expected for most applications. So the role of functional programming libraries has shifted from a compatibility layer to adding new, foundational methods for functional programming. In Mapbox Studio, we started with 101, a library that is “maintained to minimize overlap with vanilla JS.” 101 provides a lot of great functionality, especially with support of keypaths throughout, but we found that composing 101’s methods was a bit lacking.

Ramda is a library built for composition and higher-level functional programming. It tends to flip method arguments, so instead of _.map([], function() {}), ramda exposes R.map(function() {}, []), which more often makes it possible to reuse the map function. And functions in ramda are curried by default, so the R.map method could be called like R.map(function() {}, []) or R.map(function() {})([]) interchangeably.

The third generation of functional programming libraries in JavaScript is probably something like trine, which uses future syntax to write functional application in a chaining style. But ES6 is already a bit of a gamble, so Mapbox Studio isn’t ready to heavily use a strawman spec.

Currying

One of the major strengths of ramda is its approach to currying methods. Ramda’s curried methods mean that a method that looks like

foo(1, 2, 3);

Can be called with each argument in steps:

foo(1)(2)(3);
// which means
var curried = foo(1);
var alsoCurried = curried(2);
var finallyEvaluated = alsoCurried(3);

This is really nice in the case of a method like Array.map because you can partially apply a method and use it as a method in Array.map. “Partially applying the method” means you give it fewer than the full set of required arguments and supply the rest of the arguments later.

For instance, let’s take an array of objects with an ‘id’ parameter:

var data = [{ id: 'foo' }, { id: 'bar' }];

The R.prop method will give you the value of a property of a single object:

var id = R.prop('id', { id: 'foo' }); // returns 'foo'

But what about an array? The desired end result is ['foo', 'bar']. It would be great if we could just use R.prop, but have it apply against an array. Luckily, thanks to currying, we can:

var ids = data.map(R.prop('id'));

You provide the method R.prop('id') as the argument to .map: it is a function that, given an object, returns its id property.

Enough talk. Let’s see some code.


Problem: given an array of objects called layers, create an object called layerMap that has the ‘id’ property of a layer as a key and the layer itself as a value.

Solution:

var layerMap = R.zipObj(R.pluck('id', layers), layers);

Problem: you want to dynamically load a JavaScript library to power a function. In this case, we want to load the AWS SDK when someone wants to upload a file. But we want to make sure that if the method is called many times in a row, we don’t load the library as many times.

Solution: the R.once method ensures the function is only evaluated once, and on successive calls returns the same value.

var getAWS = R.once(function() {
  return new Promise(function(resolve, reject) {
    loadScript('/bundle-aws.min.js', function(err) {
      if (err || typeof AWS === 'undefined') {
        return reject(err || new Error('An error occurred'));
      } else resolve(AWS);
    });
  });
});

Problem: given a list of layers from a Mapbox GL Style Spec style, find groups of more than one layer that have a specific set of properties in common.

Solution:

var refProps = ['type', 'source', 'source-layer',
  'minzoom', 'maxzoom', 'filter', 'layout'];

// takes a layer object, returns a stripped-down version
var pickRequired = R.pick(refProps);

// compose creates a function like JSON.stringify(pickRequired(layer))
var getRefHash = R.compose(JSON.stringify.bind(JSON), pickRequired);

var candidates = R.values(R.groupBy(getRefHash, layers))
  // this is like function(l) { return l.length > 1 }, except point-free
  .filter(R.compose(R.lt(1), R.length))
  // turn a list of objects into a list of their id properties
  .map(R.pluck('id'));

Problem: given a geographical extent as an array of floating-point numbers, return an array of numbers with fixed decimal precision

Solution: note how the ramda way lets us express problems like this in point-free style.

var fixedBounds = bounds.map(R.invoker(1, 'toFixed')(1));

We use ramda in plenty of other places. Often lets us express logic that would require anonymous functions, but without using explicit functions at all: for instance R.always(1) is the same thing as function() { return 1; }, but simpler and more literal.

Try out ramda: it’s well-documented, practical, and lets you brag about functional programming to your friends.

Posted Aug 27, 2015 Tweet / Follow me on Twitter

Tom MacWright

I'm . I work on tools for creativity at Mapbox. This is where I write about technology and everything else.