Functional JS #6: Function composition

Krzysztof Czernek
DailyJS
Published in
6 min readJun 26, 2019

This is a sixth part of the “Functional JS” series. Go to the previous part here, or the beginning of the series here.

Photo by Deleece Cook on Unsplash

Introduction

Up to this point in the series, we have been focusing on techniques and vocabulary connected to Functional Programming and trying to understand how these would work in JavaScript.

While discussing that, we have repeatedly alluded to function composition: how having small functions makes for easier composition, how currying affects the way we compose functions, etc.

Now, we will try to look at the full picture. We will get to the bottom of what function composition is, and how it can help us write code that is easier to read, understand, and evolve.

Function composition, huh?

In the most general terms, we can break the whole discipline of programming down into a couple of stages:

  1. understanding complex business logic of a problem,
  2. breaking the problem down to a set of smaller problems,
  3. solving the smaller problems one at a time, and
  4. putting it back together to form a coherent solution.

Number 4 on this list — composing programs from smaller logical units — is one of the most important and challenging aspects of software engineering.

Function composition is the main tool that helps us achieve that in the functional style of programming.

Ways of composing functionality

While programming, we often need to perform a couple of operations on the same thing (be it an object, a collection, a number, etc.) one after another. For example:

  • We have a number x = 7, and we want to add 1 to it, and then square the result (and get 64), or
  • We have an array arr = [1, 2, 3, 4], and we want to filter out the odd numbers, and then calculate a sum of all the remaining numbers (and get 6).

To do that, we need to be able to express a sequence of operations in code.

In maths, we can do that relying on operator precedence and parentheses, like so:

(x + 1) ^ 2

In an object-oriented flavor of programming, we could chain method calls:

When it comes to functional programming, we would typically strive for small, reusable functions. We could then express a chain of operations by composing these functions, like in the example below:

All these examples represent ways in which we can express performing multiple operations on a single entity.

Function composition

In the last code snippet, we have seen an example of function composition. The way sumEven works is that it first calls filterOutOdd on a collection, which returns the collection stripped of odd numbers. Then we pass that to sum, which returns a sum of its elements.

Fundamentally, we can say that sumEven uses simple functions to model a more complex behavior. This is a basic pattern of function composition that is commonly used.

We can express it more generally as:

This functionality of calling the first function and passing its result directly to a second function can be extracted to avoid code repetition. Let’s create a compose function that represents this concept:

Let’s now rewrite the sumEven function above to make use of compose:

Of course, there may be more than two functions we want to compose — and this is where we get the benefit of avoiding all the nested parentheses. Let’s write a version of compose that works for three functions:

We can generalize this concept for any number of functions:

The ordering seems… off?

If you read the compose(addOne, double, square)(2) line above and thought about how it works, you might have cringed a little – the order of functions seems off. The functions are actually applied right-to-left: first 2 is squared, then it is doubled, and then addOne is called.

The way compose is used reflects the order of functions as would be written in a regular manner: addOne(double(square(2))) === compose(addOne, double, square)(2).

If you think it would be more intuitive to read the functions in the order they are being called, then pipe is your friend. It works exactly as compose, but applies the functions left-to-right.

Libraries to the rescue

Since FP is all about reusing and composing functionality, it’s not a surprise that most utilities have already been implemented in open source libraries. compose and pipe utilities are one of the most important building blocks of functional programming, so you will find them in most FP libraries. For example:

Back to currying!

There is one detail we’ve glossed over with our examples. All of them were based on functions that are unary (only accept one argument): double, addOne, filterOutOdd, etc. How convenient.

The real world is not that pretty though. Let’s try composing functions that accept more than one argument (like map):

If you ask me, the map(map(messages, getLength), double) part seems hard to process. How do we make it more elegant and abstract away all the nesting?

Looking at the definitions of both pipe and compose above, we can see that functions they operate on have to be unary.

Let’s make some unary functions!

This is a similar trick that we have used before with filterOutOdd.

Now, we can use the technique from the previous part — currying — to avoid introducing intermediary mapToLengths and mapToDoubles functions. Let's create a curried version of map:

As you can see, we chose map to accept fn and collection in that order, to make using them more intuitive in the last line.

What kind of functions to compose?

As we could see, it’s easiest to compose functionality out of functions that are:

  • unary — ones that accept just one argument,
  • small — to achieve more reusability,
  • pure — this is especially important when composing complex functionality, as function impurity is “contagious”,
  • curried, and accepting data as the last argument — to make expressions more concise.

The last point is one of the design choices of Ramda — the FP utility library mentioned above. This is why it is a really popular choice within the Functional JS community.

Is it worth it?

You may now be thinking (not for the first time, I’m sure) if all this functional mumbo-jumbo is really worth it.

When it comes to function composition, we need to think about what we are actually evaluating, and what the alternatives are.

On the face of it, composition is just a way of creating complex functionality out of simpler blocks. It helps us create abstractions. It makes it possible to group the units of application logic into a coherent whole and keep the complexity in check.

Of course, you need to consider whether using tools like pipe or compose makes sense on a case-by-case basis.

For example, I couldn’t possibly argue that the second, functional-like approach is more readable than the first one here:

On the other hand, the functional style looks really elegant here if you asked me:

This code is shorter, reads almost like plain English, and you don’t have to worry about naming all these intermediary variables (something we will get back to).

You have to make your own judgments and use the tools you know, but not just for the sake of it.

We have already discussed why writing small and pure functions is beneficial. We’ve seen how FP techniques can help us write more maintainable code. If you buy into that, then the case for using compose or pipe is simple: it is the way we put small pieces of functionality together to form a coherent solution.

Summary

We now know what we meant when we discussed function composition and how useful it is. We have therefore added another tool to our toolbox — one that is uncomplicated, but also powerful. I am sure once you start using compose and pipe and get used to them, you will never want to come back

Next time, we will discuss one additional aspect that we have glossed over here — point-free style. See you there!

--

--