Skip to content

Instantly share code, notes, and snippets.

@csauve
Last active September 19, 2017 20:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save csauve/d59dfbc7a87fa0b0fbedbb36b8aa1370 to your computer and use it in GitHub Desktop.
Save csauve/d59dfbc7a87fa0b0fbedbb36b8aa1370 to your computer and use it in GitHub Desktop.

Functional JS & Ramda


What is functional JS?

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 or Object.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

Functional JS ❤️ ES6

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)" } */

Vanilla JS

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]

Ramda 🐏

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 Helpers

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]}

Composition 🎁

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"

Currying 🍛

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"

Immutable Data Manipulation

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 🔍

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: []]}}

Links

Resources

Demos

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment