Yeison Daza
5 min read

Composing Functions in JavaScript

To build applications that solve complex problems, we need to break them down into small problems we can solve and implement, then compose those solutions together. This is what we do every day as programmers, and today we’ll see how to do this using functions in JavaScript.

Ever since subroutines and structured programming were invented, we’ve been using this concept of creating blocks of code that we can compose. In object-oriented programming, everything is about composing objects.

Composition is the essence of programming.

In functional languages, composition is done using functions. The theoretical ideas that serve as the foundation for this can also be implemented in JavaScript.

Composing in JavaScript

Function composition is based on combining simple functions to build more complex functions. To do this, the result of each function is passed as the argument to the next one.

“Function composition is the act of directing the result of one function to the input of another, creating a new function.” Haskell Wiki

Since JavaScript implements higher-order functions and closures, this is pretty straightforward to achieve. We can write it like this:

const compose = (f, g) => (x) => f(g(x));

With the function above, we can compose a function from two functions. To do it with more functions, we can rewrite this function or use a library like Ramda, which has a compose method that lets us compose a function from any number of functions.

In mathematics, function composition is defined as: (f . g)(x) = f(g(x)).

We should keep in mind:

  • Functions execute from right to left in the order they’re passed to the compose function.
  • The data type that results from one function must be the same type that the next function accepts as input.

The main idea is that each function solves a small problem, and then we compose a function that solves what we actually need to do.

Let’s see how this works with code.

For the following functions, we’ll use a list of data that you can see at this link.

Case 1

// Calcular el promedio de ingresos de todos los usuarios.
import { prop, map, reduce, add, compose } from 'ramda';
const average = (xs) => reduce(add, 0, xs) / xs.length;
const incomesAverage = compose(average, map(prop('incomes')));
incomesAverage(USERS) // 8333.333
  • Functions that receive more than one argument should be curried.
  • It’s a good idea to separate pure functions from those with side effects.

Case 2

// Retornar el nombre del usuario con mejores ingresos
import { compose, sortBy, prop, last } from 'ramda';
const bestIncomes = compose(prop('name'), last, sortBy(prop('incomes')));
bestIncomes(USERS); // Laura Mantilla
  • Data flows from right to left through the functions, with each function processing the data it receives.

Case 3

/*
 * Retornar los nombre en minúscula y
 * reemplazando espacios por underscores(_)
 */
import { compose, replace, toLower, prop, map } from 'ramda';
const underscore = replace(/\s/g, '_')
const nameToLower = compose(toLower, prop('name'))
const toCamelCase = map(compose(underscore,namesToLower))

toCamelCase(USERS)
// ['yeison_daza', 'camilo_suarez', 'laura_mantilla']
  • A key aspect of composition is that we can group functions however we want and keep composing increasingly complex functions.

Case 4

/*
 * Comprobar si alguien es un usuario
 */
import { contains, map, prop, compose } from 'ramda';
const toUsers = map(prop('nick'));
const isUser = function(user, data) {
    return compose(contains(user), toUsers)(data);
}
isUser('yeion7', USERS); // true
isUser('nata1', USERS); // false
  • Each function should expect only one argument and operate on it, which is why working with curried functions is so important.

Generic functions

When composing functions, it’s important to think about processing abstract data — that is, a function will process a type of data, but we don’t know what specific data it will be. This style is called point-free. Let’s look at this example taken from the mostly adequate guide:

// No es point free porque esta especificando que es una palabra lo que espera
var snakeCase = function(word) {
  return word.toLowerCase().replace(/\s+/ig, '_');
};
// Si es pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

This gives us generic functions.

What we’re going for here is creating pieces of code that, when we use them, we don’t think about the internal details of how they work — we just focus on what types they accept and return.

We can’t do this with every function, but it’s important to aim for creating functions in this style.

Next week I’ll talk about how to document these functions.

Category theory

Function composition is grounded in category theory. The ideas from this field have been used by the functional programming community for a long time.

A category can be represented as:

A category is composed of:

  • Objects are data of any type (String, Boolean, Number, etc.), in this case X, Y, Z
  • Morphisms are pure functions, in this case f, g
  • Composition of the morphisms, in this case a function composed of f and g
  • An identity morphism — each object has a function that returns its own value.

Properties

  • Composition is associative: if you have three or more functions, they can be grouped in any way.

h . (g . f) = (h . g) . f = h . g . f

It doesn’t matter how we group the elements — the result will always be the same.

  • Each object has an identity function: identity is a function that returns the same value it receives.
const identity = (x) => x;

In category theory, it’s not important to see how each object works internally. All we care about is how they relate to each other.

Not all objects can be composed. The type produced by a morphism must be the same type that the next morphism receives.

Conclusions

Knowing how to decompose a problem into small problems that we can solve is fundamental.

I believe that composition is an excellent way to write elegant, maintainable code, creating pieces of code that are the right size and from which we build processes we can understand.

It also helps us manage side effects, which we can separate out and know exactly where they execute.