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.
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
:
Can we compose these functions to create the pipeline from above? Let's see:
-
f
takes aBlueSquare
as input, then returns aGreenCircle
-
g
takes aGreenCircle
as input (therefore, it can be composed withf
, iff
comes first), then returns aRedDiamond
-
h
takes aRedDiamond
as input (therefore, it can be composed withg
, ifg
comes first), then returns aPurpleHexagon
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!
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?
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 aPurpleHexagon
. For example,isEven
andisOdd
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
:
And since this pipeline is a function, we can actually define the previous pipeline using this one and f
:
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
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)))
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
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:
Or more concretely:
const isGreaterThan =
(n: number, limit: number): boolean => n > limit
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]
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())
}
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())
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)
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)