JavaScript is a multi-paradigm language, supporting imperative, object-oriented, and functional styles. This presentation is intended to add some tools to your toolbelt. With familiarity of its patterns, functional JS can be very expressive. These patterns often follow from JS best practices:
- Don't extend
Array.prototype
orObject.prototype
with nonstandard functionality; use utility functions - Avoid shared or global state
- Prefer pure functions without side-effects
- Don't mutate function parameters
- Small, simple functions are easy to understand and test
- DRY through composition
New ES6 language features pair nicely with a functional style. In particular: arrow functions, structuring and destructuring assignment, template strings, and rest/spread operators. Here's a recap:
const placings = ["Sebastian", "Valtteri", "Daniel", "Kimi", "Max", "Sergio"];
const raceDetails = {id: "12345", distance: "10km", city: "Victoria"};
const formatPlacings = ([first, second, ...others]) =>
`1st: ${first}, 2nd: ${second} (${others.length} others)`;
const buildRenderParams = ({city, distance}, placings) => {
const headline = `${placings[0]} wins ${city} ${distance} race`;
return {headline, summary: formatPlacings(placings)};
};
buildRenderParams(raceDetails, placings);
/* { headline: "Sebastian wins Victoria 10km race",
summary: "1st: Sebastian, 2nd: Valtteri (4 others)" } */
Even without using any libraries, higher order functions and built-ins like Array.prototype.map
are enough to start writing functional JS:
const add = (a, b) => a + b;
const sqr = (a) => a ** 2;
const execStep = (stack, arg) => {
if (typeof arg === "function") {
const funcArgs = stack.slice(-arg.length);
const poppedStack = stack.slice(0, -arg.length);
return [...poppedStack, arg(...funcArgs)];
}
return [...stack, arg];
};
const program = [1, 2, add, sqr];
const outputStack = program.reduce(execStep, []);
//=> [9]
In larger projects, you'll find yourself repeatedly writing the same functional helpers. You could pull these out into a shared file, or you could just use the fantastic library Ramda which offers over 240 small pre-built and well tested functional building blocks.
Ramda functions live as properties under an R
object.
Usage as an NPM module:
//npm install --save ramda
const R = require("ramda");
Usage as a global in HTML:
<script src="path/to/yourCopyOf/ramda.min.js"></script>
<script>/* Use `R` */</script>
Array operations are probably sone of the most common code you write. About 1/3 of Ramda's functions, an impressive 85, are oriented around transforming sequential data. If you can think of a problem, there's probably a Ramda function for it.
const arr1 = [1, 2, 3, 4, 3, 2, 1];
const arr2 = [9, 8, 7, 6, 7, 8, 9];
R.any(R.gt(3), arr1); //=> true
R.splitEvery(3, arr1); //=> [[1, 2, 3], [4, 3, 2], [1]]
R.dropLastWhile(R.lte(3), arr1); //=> [1, 2, 3, 4]
R.zip(arr1, arr2);
//=> [[1, 9], [2, 8], [3, 7], [4, 6], [3, 7], [2, 8], [1, 9]]
R.groupBy(x => x % 2 ? "odd" : "even", arr1);
//=> {"even": [2, 4, 2], "odd": [1, 3, 3, 1]}
Use R.pipe
to create a single function representing a nesting of function calls. Each given function will be called with the return value of the last. For example, R.pipe(x, y, z)
returns a function which is equivalent to (args) => z(y(x(...args)))
. Use R.compose
to apply functions in the reverse order.
const dotProduct = R.pipe(R.zipWith(R.multiply), R.sum);
dotProduct([5, 2, 3], [4, 1, 1]); //=> 25
const reformatHeaders = R.pipe(
R.split(","),
R.map(R.pipe(
R.trim,
R.toLower,
R.replace(/[^A-Z0-9]+/gi, "_")
)),
R.join(",")
);
reformatHeaders("inventory_id, QUANTITY, item name , IS-SHIPPED");
//=> "inventory_id,quantity,item_name,is_shipped"
Curring is an extemely powerful tool that's hard to stop using once you start! It allows you to partially apply functions and get a new function of the remaining arguments. That is, calling f(a, b, c, d)
is equivalent to f(a)(b, c)(d)
.
All functions in Ramda are "curried" by default where applicable. To curry any other function, simply call R.curry
. As a general practice, order parameters by how much they vary call-to-call.
const alphanumeric = R.replace(/[^A-Z0-9]+/gi, "");
const sanitize = R.pipe(alphanumeric, R.toLower, R.take(6));
const formatUsernames = R.pipe(R.map(sanitize), R.join(", "));
const usernames = formatUsernames([
"John Doe",
"Leeeeeeeeeeeeeroy Jenkins!!!!!",
"<script>window.alert(1)</script>"
]);
//=> "johndo, leeeee, script"
Sometimes it's helpful to immutably update data structures, where a new reference is returned and the original data remains unmodified.
const obj = {};
const obj2 = R.assocPath(
["x", "y", 1],
"world",
obj
);
const arr = R.update(0, "hello", obj2.x.y)
const arr2 = R.dropLastWhile(R.test(/^w/), arr);
//obj => {}
//obj2 => {x: {y: [null, "world"]}}
//arr => ["hello", "world"]
//arr2 => ["hello"]
Some use cases for this include shared state, undo history, caches, and rendering optimizations.
Lenses pair a getter and a setter, and can be passed to R.over
to define the location of an operation on a data structure.
const todosPath = ["data", "todos"];
const getTodos = R.pathOr([], todosPath); //default of []
const setTodos = R.assocPath(todosPath);
const todosLens = R.lens(getTodos, setTodos);
const todosOp = R.curry((op, state) => R.over(todosLens, op, state));
const addTodo = (todo, state) => todosOp(R.append(todo), state);
const reverseTodos = todosOp(R.reverse);
const sortTodos = todosOp(R.sortBy(R.toLower));
const clearTodos = todosOp(R.empty);
let state = {};
state = addTodo("walk dog", state);
state = addTodo("book hotel", state);
state = addTodo("go shopping", state);
//=> {data: {todos: ["walk dog", "book hotel", "go shopping"]}}
state = reverseTodos(state);
//=> {data: {todos: ["go shopping", "book hotel", "walk dog"]}}
state = sortTodos(state);
//=> {data: {todos: ["book hotel", "go shopping", "walk dog"]}}
state = clearTodos(state);
//=> {data: {todos: []]}}