A small library of functional composition utilities.
Your standard issue, right-to-left function composition combiner. Given any number of unary functions, compose
returns a function that takes a single argument and passes it through the passed-in functions one at a time, passing the result of one function into the next function. The result will be a sequential application of all functions to the argument.
compose: ([a -> b]) -> a -> b
const first = str => str[0];
const toUpper = str => str.toUpperCase();
const getInitial = compose(toUpper, first);
console.log(getInitial('bob smith')) // B
The same as compose
but functions are applied left-to-right.
pipe: ([a -> b]) -> a -> b
const first = str => str[0];
const toUpper = str => str.toUpperCase();
const getInitial = pipe(first, toUpper);
console.log(getInitial('bob smith')) // B
Function composition for side-effects. The resulting function returns void
. Each function receives the same argument passed in to the generated function. Very useful for composing DOM event handlers. Functions are processed left-to-right
sequence: ([a -> void]) -> a -> void
const track = event => {
const { eventName, eventId } = event.target.dataset;
fetch('http://example.com/track', { method: 'POST', body: `eventName=${eventName}&eventId=${eventId}`});
};
const spaNavigate = event => {
const { href } = event.target;
history.push(href);
};
<a href="/foo" onClick={sequence(track, spaNavigate)} data-event-name="bar" data-event-id="1234">Foo</a>
Compose predicate functions into a predicate function that returns true
if every passed-in predicate returns true
. If just one returns false
, the whole returned function returns false
and no further passed-in predicates will be run. Predicate functions are processed left-to-right.
aand: ([a -> Boolean]) -> a -> Boolean
const isString = x => typeof x === 'string';
const isEmail = aand(isString, x => /^[a-zA-Z0-9\._-]+@[a-zA-Z0-9\._-]+\.[a-z]{2,}$/.test(x));
console.log(isEmail(1)) // false. Not a string
console.log(isEmail('bob')) // false. Not a a valid email format
console.log(isEmail('[email protected]')) // true
const isDomainEmail = domain => aand(isEmail, x => x.split('@')[1] === domain);
const isExampleDotComEmail = isDomainEmail('example.com');
console.log(isExampleDotComEmail(1)) // false. Not a string
console.log(isExampleDotComEmail('bob')) // false. Not a a valid email format
console.log(isExampleDotComEmail('[email protected]')) // false. Wrong domain
console.log(isExampleDotComEmail('[email protected]')) // true
Compose predicate functions into a predicate function that returns true
if one passed-in predicate returns true
and no further passed-in predicates will be run. If all return false
, the whole returned function returns false
. Predicate functions are processed left-to-right.
oor: ([a -> Boolean]) -> a -> Boolean
const isUndefined = x => x === undefined;
const isNull = x => x === null;
const isNil = oor(isUndefined, isNull);
console.log(isNil(1)) // false
console.log(isNil(null)) // true
console.log(isNil(undefined)) // true
const isFalse = x => x === false;
const isEmptyString = x => x === '';
const isZero = x => x === 0;
const isFalsey = oor(isNil, isFalse, isEmptyString, isZero);
console.log(isFalsey([])); // false. Arrays are never falsey
console.log(isFalsey(1)); // false. Non-zero numbers are not falsey
console.log(isFalsey(null)); // true
console.log(isFalsey(0)); // true
console.log(isFalsey('')); // true
console.log(isFalsey(false)); // true
Why have both compose
and pipe
? Primarily this is a matter of taste, depending on how you like to write you compositions. compose
comes from functional programming languages where the right-to-left application is based on mathematical foundations. Additionally, its ordering looks very similar to the naive method of passing the result of one function to the next:
// Naive composition
const getInitial = x => toUpper(first(x));
// Using `compose`
const getInitial = compose(toUpper, first);
pipe
is helpful because, when using it to define a function, the left-to-right ordering aligns with cultures that are used to reading left-to-right.
// Take the first character, then uppercase it
const getInitial = pipe(first, toUpper);
// A more complex example: split the string on whitespce, then take the first 2 entries, then apply `getInitial` to each entry, then join the entries with an empty string.
const getInitials = pipe(split(/\s+/), take(2), map(getInitial), join(''));
My rule of thumb for when to use each one is this:
compose
: Because it looks cleaner (to me) than naively nesting functions, I use it when I need to create a one-off data transformation pipeline that will be assigned to a variable. The raw data starts on the far right and moves one function at a time to the left and is finally assigned to a variable at the far left.
// assign to `initials` <- do join <- do map <- do take <- do split <- name
const initials = compose(join(''), map(getInitial), take(2), split(/\s+/))(name);
pipe
: I use it when I'm defining reusable functions because most code is written to be read left-to-right, so this aligns more closely (for me) with my native English familiarity.
// Split, then take, then map, then join
const getInitials = pipe(split(/\s+/), take(2), map(getInitial), join(''));