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