Thinking in Ramda: Combining Functions

This post is Part 2 of a series about functional programming called Thinking in Ramda.

In Part 1, I introduced Ramda and some of the basic ideas about functional programming, such as functions, pure functions, and immutability. I then suggested that a good place to start is with the collection-iteration functions such as forEach, map, filter, reduce and friends.

Simple Combinations

Once you’ve gotten used to the idea of passing functions to other functions, you might start to find situations where you want to combine several functions together.

Ramda provides several functions for doing simple combinations. Let’s look at a few.

Complement

In the last post, we used find to find the first even number in a list:

find

const isEven = x => x % 2 === 0
find(isEven, [1, 2, 3, 4]) // --> 2

What if we wanted to find the first odd number instead. We could always write an isOdd function and use it, but we know that any number that isn’t even is odd. Let’s reuse our isEven function.

Ramda provides a higher-order function, complement, that takes another function and returns a new function that returns true when the original function returns a falsy value, and false when the original function returns a truthy value.

find with complement

const isEven = x => x % 2 === 0
find(complement(isEven), [1, 2, 3, 4]) // --> 1

Even better is to give the complemented function its own name so it can be reused:

isOdd with complement

const isEven = x => x % 2 === 0
const isOdd = complement(isEven)
find(isOdd, [1, 2, 3, 4]) // --> 1

Note that complement implements the same idea for functions as the ! (not) operator does for values.

Both/Either

Let’s say we’re working on a voting system. Given a person, we’d like to be able to determine if that person is eligible to vote. Based on our current knowledge, a person must be at least 18 years old and be a citizen in order to be able to vote. Someone is a citizen if they were born in the country or if they later became a citizen through naturalization.

Eligible Voters

const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY
const wasNaturalized = person => Boolean(person.naturalizationDate)
const isOver18 = person => person.age >= 18
const isCitizen = person => wasBornInCountry(person) || wasNaturalized(person)
const isEligibleToVote = person => isOver18(person) && isCitizen(person)

What we’ve written above works, but Ramda provides a couple of handy functions to help us clean it up a bit.

both takes two other functions and returns a new function that returns true if both functions return a truthy value when applied to the arguments and false otherwise.

either takes two other functions and returns a new function that returns true if either function returns a truthy value when applied to the arguments and false otherwise.

Using these two functions, we can simplify isCitizen and isEligibleToVote:

Using both and either

const isCitizen = either(wasBornInCountry, wasNaturalized)
const isEligibleToVote = both(isOver18, isCitizen)

Note that both implements the same idea for functions as the && (and) operator does for values, and either implements that same idea for functions as the || (or) operator does for values.

Ramda also provides allPass and anyPass that take an array of any number of functions. As their names suggest, allPass works like both, and anyPass works like either.

Pipelines

Sometimes we want to apply several functions to some data in a pipeline fashion. For example, we might want to take two numbers, multiply them together, add one, and square the result. We could write it like this:

Pipeline the hard way

const multiply = (a, b) => a * b
const addOne = x => x + 1
const square = x => x * x
const operate = (x, y) => {
  const product = multiply(x, y)
  const incremented = addOne(product)
  const squared = square(incremented)
  return squared
}
operate(3, 4) // => ((3 * 4) + 1)^2 => (12 + 1)^2 => 13^2 => 169

Notice how each operation is applied to the result of the previous one.

pipe

Ramda provides the pipe function, which takes a list of one or more functions and returns a new function.

The new function takes the same number of arguments as the first function it is given. It then “pipes” those arguments through each function in the list. It applies the first function to the arguments, passes its result to the second function and so on. The result of the last function is the result of the pipe call.

Note that all of the functions after the first must only take a single argument.

Knowing this, we can use pipe to simplify our operate function:

Using pipe

const operate = pipe(
  multiply,
  addOne,
  square
)

When we call operate(3, 4), pipe passes the 3 and 4 to the multiply function, resulting in 12. It passes that 12 to addOne, which returns 13. It then passes that 13 to square, which returns 169, and that becomes the final result of operate.

compose

Another way we could have written our original operate function is to inline all of the temporary variables:

Inlined Pipeline

const operate = (x, y) => square(addOne(multiply(x, y)))

That’s much more compact, but somewhat harder to read. In that form, however, it lends itself to be rewritten using Ramda’s compose function.

compose works exactly the same way as pipe, except that it applies the functions in right-to-left order instead of left-to-right. Let’s write operate with compose:

Using compose

const operate = compose(
  square,
  addOne,
  multiply
)

This is exactly the same as pipe above, but with the functions in the opposite order. In fact, Ramda’s compose function is written in terms of pipe.

I always think of compose this way: compose(f, g)(value) is equivalent to f(g(value)).

As with pipe, note that all of the functions except the last must only take a single argument.

compose or pipe?

I think that pipe is probably the easiest to understand when coming from a more imperative background since you read the functions left-to-right. But compose is a bit easier to translate to nested-function form as I showed above.

I haven’t yet developed a good rule for when I prefer compose and when I prefer pipe. Since they are essentially equivalent in Ramda, it probably doesn’t matter which one you choose. Just go with whichever one reads the best in your situation.

Conclusion

By combining several functions in specific ways, we can start to write more powerful functions.

Next

You may have noticed that we mostly ignored the function arguments when we were combining functions. We only supply the arguments when we finally call the combined function.

This is common in functional programming, and we talk about that a lot more in the next post in this series, Partial Application. We also talk about how to combine functions that take more than one argument.