Utility libraries like Ramda and lodash/fp make functional programming (FP) very accessible. To use these libraries effectively, an understanding of what's going on under the hood is still required, and a good grasp of FP concepts goes a long way
lodash/fp: an instance of lodash with its methods wrapped to produce immutable auto-curried iteratee-first data-last methods - lodash/fp Guide
Ramda: a library designed specifically for a functional programming style, one that makes it easy to create functional pipelines, one that never mutates user data - Ramda
The FP Penny Drops
Up until two years ago I had programmed in a very imperative object-orientated style. From time to time I had read articles on functional programming, but it seemed like an esoteric topic - not something I could see myself using in practice
It wasn't until I was introduced to Ramda
and went through a shallow learning curve (with the x axis representing time😉) that I understood the practicality of FP concepts. I began to see functional composition, higher-order functions, and FP in general, in a new light
In this article I'll detail the concepts and features I learned along the way. What I found especially helpful was was creating vanilla ES6+
versions of key Ramda
and lodash/fp
functions - this was where it all came together for me - so I'll add in some examples of those at the end
Auto Currying
One of the core features of FP libraries is auto-currying, where only some of a functions' parameters are supplied when it's invoked, with a returned function expecting the remaining ones
Lodash Currying
Given the following collection
, we want a reusable function that returns an Array of all of the code
property values i.e. ["A", "B"]
:
const collection = [
{ id: 1, code: 'A' },
{ id: 2, code: 'B' }
];
lodash (no currying)
const getCodes = arr => {
// no currying, wrapper function required
return _.map(arr, 'code');
};
getCodes(collection) // ["A", "B"]
lodash (with currying)
const getCodes = _.curryRight(_.map, 2)('code');
getCodes(collection) // ["A", "B"]
lodash/fp (auto-curried)
// fp.map iteratee can be a function or property key
const getCodes = fp.map('code');
getCodes(collection) // ["A", "B"]
Function-First, Data-Last
Auto-currying ties in closely with another fundamental aspect of FP libraries: function-first, data-last
Info: in functional programming parlance the function here is referred to as an iteratee, and the data as a functor (anything that can be mapped over)
Lodash Param Order
Given the following collection
, we want a reusable function that returns an Array of all of the values incremented by 10
i.e. [10, 11, 12]
:
const collection = [0, 1, 2];
lodash (data-first)
const add10 = value => _.add(value, 10);
// _.map is data-first
const incrementData = arr => _.map(arr, add10);
incrementData(collection) // [10, 11, 12]
lodash/fp (function-first)
// fp.map is auto-curried, function-first, data-last
const incrementData = fp.map(fp.add(10));
incrementData(collection) // [10, 11, 12]
Immutable
All Ramda
and lodash/fp
functions are immutable - they never mutate/change the data passed in. New arrays and objects are returned
lodash (mutate)
const originalData = [1, 2, 3];
const reversed = _.reverse(originalData);
// originalData now [3, 2, 1]
// originalData === reversed
Ramda & lodash/fp (immutable)
const originalData = [1, 2, 3];
const reversed = R.reverse(originalData);
// originalData remains the same
// originalData !== reversed
Composition
Ramda makes it simple for you to build complex logic through functional composition - Why Ramda?
Both Ramda
and lodash/fp
provide a compose
function, allowing you to combine multiple functions. compose
performs right-to-left composition
Function composition (without compose)
const input = ['1', '2', '3', 'A'];
// Note: Number('A') returns NaN
const toNumbers = arr => arr.map(x => Number(x));
const rejectNaN = arr => arr.filter(x => !isNaN(x));
const sum = arr => arr.reduce((acc, val) => acc + val, 0);
const sumArray = arr => sum(rejectNaN(toNumbers(arr)));
sumArray(input); // 6
ES6 compose (basic)
const input = ['1', '2', '3', 'A'];
const compose = (...fns) => x =>
fns.reduceRight((acc, fn) => fn(acc), x);
// using pervious toNumbers, rejectNaN, sum
const es6SumArray = compose(
sum,
rejectNaN,
toNumbers
);
es6SumArray(input); // 6
Ramda compose
const input = ['1', '2', '3', 'A'];
const ramdaSumArray = R.compose(
R.sum,
R.reject(R.equals(NaN)),
R.map(Number)
);
ramdaSumArray(input); // 6
R.compose: the rightmost function may have any arity (any number of parameters); the remaining functions must be unary (single parameter functions) - Ramda Compose
R.pipe: the same as R.compose, but function composition is performed left-to-right Ramda Pipe
FP Concepts CodeSandbox
ES6 FP Utilities
Initially I wrote FP utility functions to learn how Ramda
works, but I have seen similar ones used in npm
modules, where the contributors didn't want to require any external dependencies. I've seen variations of ES6
compose
a number of times (Redux compose.js)
Info: The DomLog npm library used in the CodeSandbox embed is one I published myself, and you can see an example of a compose function used there
There is no auto-currying implemented in the following examples - these are simple higher-order functions with fixed arity. Also, the functions tend to focus on array
transformation, whereas Ramda
can transform both objects
and arrays
e.g. R.map
R.map(double, {x: 1, y: 2, z: 3}); //=> {x: 2, y: 4, z: 6}
Info: implementation of R.map
ES6 basic FP utilities
These are some ES6
equivalents of a few common Ramda
functions - they would be used in conjuntion with compose
const compose = (...fns) => x =>
fns.reduceRight((acc, fn) => fn(acc), x);
const map = fn => (arr = []) => arr.map(fn);
const prop = p => (obj = {}) => obj[p];
const pluck = p => arr => map(prop(p))(arr);
const flatten = (arr = []) =>
[].concat.apply([], arr);
const isNil = val =>
val === null || val === void 0;
const complement = fn => val => !fn(val);
const rejectNil = (arr = []) =>
arr.filter(complement(isNil));
const flip = fn => a => b => fn(b)(a);
const identity = x => x;
const tap = fn => val => {
fn(val);
return val;
};
ES6 complex FP functions
groupBy
is available in both Ramda
and lodash/fp
, but evolve is specific to Ramda
. There is no simple equivalent to evolve
in lodash/fp
as far as I can tell
groupBy
const groupBy = p => arr =>
arr.reduce((acc, obj) => {
const value = obj[p];
acc[value] = [...(acc[value] || []), obj];
return acc;
}, {});
Description of Ramda evolve
from the documentation: Creates a new object by recursively evolving a shallow copy of object, according to the transformation functions. All non-primitive properties are copied by reference. A transformation function will not be invoked if its corresponding key does not exist in the evolved object
evolve
const evolve = transformation => obj => {
const transformed = Object.entries(transformation).reduce(
(acc, [key, value]) => {
if (!obj.hasOwnProperty(key)) return acc;
acc[key] =
typeof value === 'function'
? value(obj[key])
: evolve(value)(obj[key]);
return acc;
},
{}
);
return { ...obj, ...transformed };
};
ES6 evolve demo:
// Evolve
const unixToISOString = unix =>
new Date(unix).toISOString();
const record = {
date: 799286400000, //Unix time stamp in ms
details: {
enabled: false,
version: 1
}
};
const evolveRecord = evolve({
date: unixToISOString,
details: {
enabled: complement(identity)
}
});
const newRecord = evolveRecord(record);
/*
{
"date": 799286400000,
"details": {
"enabled": false,
"version": 1
}
}
{
"date": "1995-05-01T00:00:00.000Z",
"details": {
"enabled": true,
"version": 1
}
}
*/