DEV Community

Cover image for Function composition and higher-order function
Benoit Ruiz
Benoit Ruiz

Posted on • Edited on

Function composition and higher-order function

Table of contents


Function composition

Function composition is a simple yet powerful key concept in Functional Programming.

What is it?

In a nutshell, it's the ability to combine behaviors together in a specific order, transforming data little by little from an initial shape into a new one.

More concretely, it's the combination of functions where the output (returned value) of the previous function becomes the input (argument) of the next one, and so on until the last function.

We can visualize it as a pipeline where some data of a given shape enters on one side, then comes out with a new (same or different) shape on the other side.

A blue square goes inside an opaque pipe on one side, then comes out as a purple hexagon on the other side

Composing functions yields... Another function! Therefore a pipeline is, in turn, a function. We don't know exactly how it's defined from an external point of view, and that's why it's so powerful: it's an abstraction that hides implementation details. The intermediate steps to transform a BlueSquare into a PurpleHexagon are hidden/abstracted.

Given the following 3 functions f, g, and h:

3 functions are defined:

Can we compose these functions to create the pipeline from above? Let's see:

  • f takes a BlueSquare as input, then returns a GreenCircle
  • g takes a GreenCircle as input (therefore, it can be composed with f, if f comes first), then returns a RedDiamond
  • h takes a RedDiamond as input (therefore, it can be composed with g, if g comes first), then returns a PurpleHexagon

By aligning f, then g and finally h, we end up with a pipeline that takes a BlueSquare as input, and returns a PurpleHexagon, which is exactly what we were looking for!

Composition of the 3 functions

The weird notation with circles is the way function composition in mathematics is written: h ∘ g ∘ f can be read as "h round g round f", and is applied from right to left: h(g(f(x))).

How far we can go with it

Since pipelines are functions, it means we can compose them with other functions as well. This is the essence of software programming: composing smaller pieces together to end up with a complex system that fulfills all the requirements.

Suppose we need to transform some GreenCircle into a PurpleHexagon somewhere in our code base. Do we have to implement this function by ourselves, or can we simply compose functions that already exist?

Can we compose the functions defined earlier to come up with a new function that takes a green circle as input, and returns a purple hexagon?

Here, we are assuming the actual implementations of the functions from above suit our needs. In reality, there could be several implementations for a function transforming a GreenCircle into a PurpleHexagon. For example, isEven and isOdd are both functions that transform numbers (green circles) into booleans (purple hexagons), but with different implementations.

Of course we can, by composing g with h:

The composition of

And since this pipeline is a function, we can actually define the previous pipeline using this one and f:

The composition of the function

This way, we have reusable units/functions in our code base that can be composed anywhere to form new functions/pipelines that are more and more complex.

Shapes are nice, but if you're looking for something more concrete, here's an example with simple functions in TypeScript:

const getNumberOfCharacters =
  (text: string): number => text.length

const increment = 
  (n: number): number => n + 1

const isGreaterThan5 =
  (n: number): boolean => n > 5

// pipeline definition
const isTextLongEnough: (text: string) => boolean =
  flow(getNumberOfCharacters, increment, isGreaterThan5)

console.log(isTextLongEnough('abc')) // false
console.log(isTextLongEnough('abcde')) // true
Enter fullscreen mode Exit fullscreen mode

I'm using the flow function from the fp-ts library, which is used specifically to compose functions, from left to right. I could've written it the following way, but it's less readable in my opinion:

const isTextLongEnough: (text: string) => boolean =
  text => isGreaterThan5(increment(getNumberOfCharacters(text)))
Enter fullscreen mode Exit fullscreen mode

The advantage of having small units/functions is that we can reuse some of them to create different pipelines. For example:

const isEven =
  (n: number): boolean => n % 2 === 0

const hasEvenNumberOfCharacters: (text: string) => boolean =
  flow(getNumberOfCharacters, isEven)

console.log(hasEvenNumberOfCharacters('abc')) // false
console.log(hasEvenNumberOfCharacters('abcd')) // true
Enter fullscreen mode Exit fullscreen mode

Wait a minute... All these functions have only 1 argument, also called unary functions. What happens if we want to compose functions that take multiple arguments? For example:

Function that takes 3 arguments (in the order: yellow star, green circle, and cyan diamond) and returns a red diamond

Or more concretely:

const isGreaterThan =
  (n: number, limit: number): boolean => n > limit
Enter fullscreen mode Exit fullscreen mode

The answer lies in currying and partial application, which are 2 concepts we'll see later in this series. Stay tuned!

Higher-order function

A higher-order function, or HOF for short, is a function that takes at least a function as its argument(s), and/or returns a new function.

If you have used the map and filter methods on an array in JavaScript, then you have already used HOFs, because these 2 methods take a function as their arguments:

const evenNumbers = [1, 2, 3, 4].filter(n => n % 2 === 0)
// or the shorter version: [1, 2, 3, 4].filter(isEven)

console.log(evenNumbers) // [2, 4]

const doubledNumbers = [1, 2, 3].map(n => n * 2)

console.log(doubledNumbers) // [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

We can create our own higher-order functions as well. Let's have a look at a simple example, in TypeScript and Scala:

const enum Shape {
  BlueSquare,
  GreenCircle,
  RedDiamond
}

function createShapes(
  volume: number,
  generateShape: () => Shape
): Shape[] {
  return [...new Array(volume)].map(() => generateShape())
}
Enter fullscreen mode Exit fullscreen mode
sealed trait Shape
case object BlueSquare extends Shape
case object GreenCircle extends Shape
case object RedDiamond extends Shape

def createShapes(
  volume:        Int,
  generateShape: () => Shape
): List[Shape] =
  List.range(0, volume).map(_ => generateShape())
Enter fullscreen mode Exit fullscreen mode

Here the createShapes function is a HOF, because it takes a function as one of its arguments: generateShape.

If you are familiar with design patterns from Object-Oriented Programming, then you might have thought about the Strategy pattern here, and you would've been correct. In fact, a lot of OO design patterns can be implemented using HOFs. That being said, some of the OO patterns don't make sense in FP, since we are not trying to solve problems with classes and objects, but rather with data and functions.

The flow function that we saw earlier is also a HOF, since it takes multiple functions as its arguments, and returns a function at the end.

We can define a higher-order function in TypeScript to emulate some form of pattern matching on these shapes:

function matchShape(
  onBlueSquare:  (shape: BlueSquare) => void,
  onGreenCircle: (shape: GreenCircle) => void,
  onRedDiamond:  (shape: RedDiamond) => void
) {
  return (shape: Shape): void => {
    switch (shape) {
      case BlueSquare:
        return onBlueSquare(shape)
      case GreenCircle:
        return onGreenCircle(shape)
      case RedDiamond:
        return onRedDiamond(shape)
    }
  }
}

matchShape(
  shape => console.log('Blue is my favorite color!', shape),
  shape => console.log('I love green tea!', shape),
  shape => console.log('I have a red car!', shape)
)(GreenCircle)
Enter fullscreen mode Exit fullscreen mode

In this example, matchShape is a HOF as it takes 3 arguments that are functions, and returns a new function that takes a Shape as its argument.

If you are from the web app development world, you might have crossed path with Higher-Order Components, or HOCs. In React, a HOC is a function that takes a component as its argument, and returns a new component. We can clearly see the similarities with HOFs here.


Alright, we've reached the end of this chapter.

Summary

  • Function composition is the ability to combine functions together, in a specific order, to transform some shape into a new shape.
  • A higher-order function, or HOF, is a function that takes at least 1 function as its argument(s), and may return a new function.

Thank you for reading this far! I hope everything was clear and you learned something. If you have any question or need some clarifications, feel free to leave a comment :)

The next article in this series will be about "declarative vs imperative" programming. See you next time!


Photo by Xavi Cabrera on Unsplash.

Pictures created with Excalidraw.

Top comments (0)