Book notes: Functional-Light JavaScript
These are the raw notes that I took while reading Functional-Light JavaScript by Kyle Simpson.
JavaScript implementations of common functions used in functional programming
Unary
function unary(fn) {return function onlyOneArg(arg) {return fn(arg);};}
Identity
function identity(v) {return v;}
Can act as a truthy check when used as a predicate since JavaScript will coerce the value to true
or false
.
Example
identity('anything') ? doSomethingTruthy() : doSomethingFalsey();
Constant
function constant(v) {return function value() {return v;};}
Useful when you need a function to return an unchanging value.
Example
p1.then(foo).then(constant(unchangingVal)).then(bar);
Apply
function spreadArgs(fn) {return function spreadFn(argsArr) {return fn(...argsArr);};}
Useful to transform an array argument into individual args.
Unapply
function gatherArgs(fn) {return function gatheredFn(...argsArr) {return fn(argsArr);};}
Useful to transform individual args into an array of args.
Partial
function partial(fn, ...presetArgs) {return function partiallyApplied(...laterArgs) {return fn(...presetArgs, ...laterArgs);};}
Reduces the arity of fn
by creating another function where some args are preset.
Returns a function that, when invoked, will run fn
with presetArgs
and invokation args (laterArgs
).
Example
[1, 2, 3, 4, 5].map(partial(add, 3)); // [4,5,6,7,8]
Partial Right
function partialRight(fn, ...presetArgs) {return function partiallyApplied(...laterArgs) {return fn(...laterArgs, ...presetArgs);};}
Useful when you want to partially apply args from the right instead of left.
Reverse Args
function reverseArgs(fn) {return function argsReversed(...args) {return fn(...args.reverse());};}
Currying
function curry(fn, arity = fn.length) {return (function nextCurried(prevArgs) {return function curried(nextArg) {var args = [...prevArgs, nextArg];if (args.length >= arity) {return fn(...args);} else {return nextCurried(args);}};})([]);}
Currying unwinds a single higher-arity function into a series of chained unary functions. It's basically a special form of partial application where the arity is reduced to 1.
Note: If you use this implementation of curry(..)
with a function that doesn't have an accurate length
property, you'll need to pass the arity
(the second parameter of curry(..)
) to ensure curry(..)
works correctly. The length
property will be inaccurate if the function's parameter signature includes default parameter values, parameter destructuring, or is variadic with ...args
.
function looseCurry(fn, arity = fn.length) {return (function nextCurried(prevArgs) {return function curried(...nextArgs) {var args = [...prevArgs, ...nextArgs];if (args.length >= arity) {return fn(...args);} else {return nextCurried(args);}};})([]);}
Example
var curriedSum = looseCurry(sum, 5);curriedSum(1)(2, 3)(4, 5); // 15
Currying/Partial Without Ordering Dependency (Named Props)
function partialProps(fn, presetArgsObj) {return function partiallyApplied(laterArgsObj) {return fn(Object.assign({}, presetArgsObj, laterArgsObj));};}function curryProps(fn, arity = 1) {return (function nextCurried(prevArgsObj) {return function curried(nextArgObj = {}) {var [key] = Object.keys(nextArgObj);var allArgsObj = Object.assign({}, prevArgsObj, {[key]: nextArgObj[key],});if (Object.keys(allArgsObj).length >= arity) {return fn(allArgsObj);} else {return nextCurried(allArgsObj);}};})({});}
Example
function foo({ x, y, z } = {}) {console.log(`x:${x} y:${y} z:${z}`);}var f1 = curryProps(foo, 3);var f2 = partialProps(foo, { y: 2 });f1({ y: 2 })({ x: 1 })({ z: 3 }); // x:1 y:2 z:3f2({ z: 3, x: 1 }); // x:1 y:2 z:3
Note: we can only take advantage of currying with named arguments if we have control over the signature of foo(..)
and define it to destructure its first parameter.
Complement
function not(predicate) {return function negated(...args) {return !predicate(...args);};}
When
function when(predicate, fn) {return function conditional(...args) {if (predicate(...args)) {return fn(...args);}};}
Compose
function compose2(fn2, fn1) {return function composed(origValue) {return fn2(fn1(origValue));};}
Most typical FP libraries define their compose(..)
to work right-to-left in terms of ordering. The function calls are listed to match the order they are written in code manually or rather the order we encounter them when reading from left-to-right.
function compose(...fns) {return function composed(result) {// copy the array of functionsvar list = [...fns];while (list.length > 0) {// take the last function off the end of the list// and execute itresult = list.pop()(result);}return result;};}
Alternate implementation using recursion:
function compose(...fns) {// pull off the last two argumentsvar [fn1, fn2, ...rest] = fns.reverse();var composedFn = function composed(...args) {return fn2(fn1(...args));};if (rest.length == 0) return composedFn;return compose(...rest.reverse(), composedFn);}
Alternate implementation using reduce
:
function compose(...fns) {return fns.reverse().reduce(function reducer(fn1, fn2) {return function composed(...args) {return fn2(fn1(...args));};});}
List Operations
var filter = curry((predicateFn, arr) => arr.filter(predicateFn));var map = curry((mapperFn, arr) => arr.map(mapperFn));var reduce = curry((reducerFn, initialValue, arr) =>arr.reduce(reducerFn, initialValue));
You can create a generic "invoker" method to handle all 3:
var unboundMethod = (methodName, argCount = 2) =>curry((...args) => {var obj = args.pop();return obj[methodName](...args);}, argCount);var filter = unboundMethod('filter', 2);var map = unboundMethod('map', 2);var reduce = unboundMethod('reduce', 3);
Other Notes
Other notes on concepts and vocabulary relating to functional programming and/or JavaScript.
Point Free Style (AKA Tactic Programming)
Point = Parameter
Point Free = Not explicitly passing a parameter to a unary function and letting the outer function implicitly pass the param to the inner function.
Example
function double(x) {return x * 2;}[1, 2, 3, 4, 5].map(double); // [2,4,6,8,10]
Idempotence
From the mathematical point of view idempotence means an operation whose output won't ever change after the first call if you feed that output back into the operation over and over again.
Example
Math.abs(..)Math.min(..)Math.max(..)Math.round(..)Math.floor(..)Math.ceil(..)
The programming-oriented definition for idempotence is similar, but less formal. Instead of requiring f(x) === f(f(x))
, this view of idempotence is just that f(x);
results in the same program behavior as f(x); f(x);
. In other words, the result of calling f(x)
subsequent times after the first call doesn't change anything.
Closure vs Objects
A closure associates a single function with a set of state whereas an object holding the same state can have any number of functions to operate on that state.
Reduce
Warning: In JavaScript, if there's not at least one value in the reduction (either in the array or specified as initialValue
) an error is thrown. Be careful not to omit the initialValue
if the list for the reduction could possibly be empty.
Beneficial Side Effects
Side effects may be useful for performance reasons like for caching.
Example
var specialNumber = (function memoization() {var cache = [];return function specialNumber(n) {// if we've already calculated this special number,// skip the work and just return it from the cacheif (cache[n] !== undefined) {return cache[n];}var x = 1,y = 1;for (let i = 1; i <= n; i++) {x += i % 2;y += i % 3;}cache[n] = (x * y) / (n + 1);return cache[n];};})();
The caching is useful here since passing a large number to specialNumber
can be costly.
Freezing JavaScript Objects
Notable Object.freeze
behavior:
var x = Object.freeze([2, 3, [4, 5]]);// not allowed:x[0] = 42;// oops, still allowed:x[2][0] = 42;
Vocabulary
A pure function has referential transparency which is an assertion that a function call could be replaced by its output value and the overall program behavior wouldn't change.
Isomorphism in JavaScript is when you have one set of JavaScript code that is converted to another set of JavaScript code and (importantly) you could convert from the latter back to the former if you wanted.
An array is a functor because it has a functor utility (map
) that can perform an operation (operator function) on each member resulting in a transformed data structure.