My Favorite Parts of Ramda

After using Lodash for a while, I recently heard of Ramda. Ramda is another functional programming library for JavaScript. Ramda is pretty similar to Lodash in a lot of ways, so it's easy to switch between them as needed.

Deep cloning by default

Ramda does deep cloning by default with its clone method. This is great because I've never run into a situation where i preferred a shallow clone. It's awkward when the attributes of two objects are different but the attributes of the attributes of two objects are the same.

// Deep object cloning

import { clone } from 'ramda';

const drPepper = {
  ingredients: {
    count: 21
  }
};

const mrPibb = clone(drPepper);

mrPibb.ingredients.count = 15;

console.log(drPepper.ingredients.count); // 21
console.log(mrPibb.ingredients.count); // 15

Safe attribute access

When fetching deeply nested attributes, it's annoying having to guard against any attribute in the chain being undefined. Ramda has two methods that simply return undefined for missing attributes instead of throwing an error. R.prop just grabs an attribute of an object, and R.path digs through an object's nested attributes to retrieve the data you ask for.

// Safe attribute fetching

import { prop, path } from 'ramda';

const wizard = {
 name: 'Tim',
 stats: {
   hp: 20,
 },
};

prop('name', wizard) // Tim
path(['stats', 'hp'], wizard) // 20
path(['holy', 'grail'], wizard) // undefined

Ramda has all the functional basics

All the basic functional programming functions you're used to are here as well: map, reduce, filter, etc. Note that flatmap is called chain in Ramda.

// Typical functional-style functions

import { filter, map, reduce } from 'ramda';

const isOdd = n => n % 2 === 1;
const double = n => n * 2;
const dotJoin = (acc, s) => `${acc}.${s}`

const numbers = [7, 13, 42];

filter(isOdd, numbers); //[7,13]
map(double, numbers); //[14,26,84]
reduce(dotJoin, '', numbers); //.7.13.42

Ramda makes composition easy

Most (if not all) Ramda functions are auto-curried, so you can feed them their arguments one at a time or all at once. Combine this with pipe to easily compose your functions into more powerful functions.

// Using pipe to compose functions

import { pipe } from 'ramda';

const increment = n => n + 1
const isOdd = n => n % 2 === 1

const number = 7

pipe(
 increment,
 isOdd
)(number) // false

Typically, map takes two arguments: an array to work on and the function to apply to the array. In the example below, map is only given the function to apply. Only after the function created by pipe is given an array to work on (number) is map given an array to work on. This is Ramda's auto-currying at work.

// Using pipe to compose array functions

import { pipe } from 'ramda';

const increment = n => n + 1
const isOdd = n => n % 2 === 1

const numbers = [7, 13, 42]

pipe(
 map(increment),
 filter(isOdd)
)(numbers) // [43]

Debugging composed functions

Debugging composed functions can be a pain because so much goes on between the input and the output. By using tap between each step of our composed function, we can get a look at the intermediate results each step of the way.

// Using tap for debug

import { pipe, tap } from 'ramda';

const increment = n => n + 1
const isOdd = n => n % 2 === 1
const log = o => console.log(o)

const numbers = [7, 13, 42]

pipe(
 tap(log), // [7,13,42]
 map(increment),
 tap(log), // [8,14,43]
 filter(isOdd),
 tap(log), // [43]
)(numbers)

Memoization is available too

In the code below, we calculate the 40th number in the Fibonacci sequence with a recursive algorithm. This algorithm is very inefficient because the Fibonacci numbers before the 40th are calculated again and again. We have to call fib() 331,160,281 times!

// Basic Fibonacci Recursion

let count = 0;

let fib = n => {
 count += 1;
 if (n <= 1) { return 1; }
 return fib(n - 1) + fib(n - 2);
}

fib(40);
console.log(count); // 331,160,281

Memoization lets us calculate each Fibonacci number before 40 just once. After we calculate it the first time, we store it and return it directly from memory each subsequent time that we need it.

Ramda used to have a function called memoize, but it has been removed. Ramda's memoize used .toString() to convert the memoized function's arguments to strings to use as cache keys. This was very slow because some objects do a ton of formatting before dumping their contents to a string.

So now we're left with just memoizeWith which needs a function to map arguments to a cache key string and the function to memoize.

In the example below, identity is a Ramda function that just returns its argument. Since fib() accepts only integer arguments, we can use identity to return our cache keys.

// Memoized Fibonacci Recursion

import { memoizeWith } from 'ramda';

let count = 0;

let fib = n => {
 count += 1;
 if (n <= 1) { return 1; }
 return fib(n - 1) + fib(n - 2);
}

fib = memoizeWith(identity, fib)

fib(40);
console.log(count); // 41

After memoization, fib(40) is far more efficient. It only calls fib() 41 times instead of 331 million times. Much better!

What's your favorite Ramda trick? Let me know in the comments!

Photo by Livin4wheel