Functional Programming (Part 4): Functional composition
Where fun meets productivity!
This article is a part of a series that talks about “Functional Programming”
In the previous article in this series we discussed a very useful pattern; Currying and how we can use it, where we also utilised it to apply separation of concerns very elegantly. In this article we’ll be talking about Composition.
Table of contents
- What is Functional Composition
- How composition works
compose
vspipe
- Examples
- Why Composition
- Conclusion
What is Functional Composition
Composition is the process of composing small units into bigger units that solve bigger problems. Where Input of one function comes from the output of previous one.
For example, if we want to build a castle 🏰 we’ll have to compose bricks 🧱
How Composition Works
As the definition states
“Input of one function comes from the output of another function”.
If we build this mathematical function (f ∘ g)(x) = f(g(x))
, which composes 2 small functions, it’ll look like this:
const compose = (f, g) => (x) => f(g(x))
The compose
function composes f
and g
where the output of g
will be forwarded as input of f
.
Let’s give it a try:
const getAge = (user) => user.age
const isAllowedAge = (age) => age >= 30
We have getAge
and isAllowed
small functions. Let’s compose them:
const user = { name: 'John', age: 35 }
const isAllowedUser = compose(
isAllowedAge,
getAge
)
isAllowedUser(user) // true
Note: compose
flows from bottom to top (right to left). We pass user
to the composite isAllowedUser
, then user
goes through getAge
first, then the output goes to isAllowedAge
.
Compose vs Pipe
pipe
is very similar to compose
, they have the same purpose. Both are here to chain functions, however they have different implementations and workflows.
Implementation
compose
is implemented like this:
const compose =
(...fns) =>
(x) => fns.reduceRight((acc, fn) => fn(acc), x)
pipe
is implemented like this:
const pipe =
(...fns) =>
(x) => fns.reduce((acc, fn) => fn(acc), x)
Where the only difference here is reduce
instead of reduceRight
. Which only affects the flow of the data…
Flow
compose
‘s flow is from bottom to top ↑(right to left ←).
pipe
‘s flow is from top to bottom ↓ (left to right →)
Example
Let’s say that we have 3 functions, f
, g
and h
If we use compose
:
compose(f, g, h)
← ← ←
The evaluation order will go h
, g
then f
(right to left), and each output is forwarded to the next function.
If we use pipe
:
pipe(f, g, h)
→ → →
The evaluation order will go f
, g
then h
(left to right), and each output is forwarded to the next function.
Which should you use?
They don’t differ much. Both will solve the same problem.
But just to highlight the difference. compose
is just closer to the mathematical notation of (f ∘ g)(x) = f(g(x))
, where pipe
is often easier to read in evaluation order.
So personally, I prefer pipe
because it is more natural.
Examples:
Note: as a personal preference I’ll be using pipe
in the examples.
Let’s say we need to build a simple price calculator, where we can apply:
- Discount
- Coupon
- Tax (default = 30%)
- Service fees (default = 10)
- Weight-based shipping cost
Let’s first design the API of the price calculator based on the given requirements
const priceCalculator = (
taxPercentage = 0.3,
serviceFees = 10,
price,
discount,
percentCoupon,
valueCoupon,
weight,
$PerKg
) => {
/*logic*/
}
We defaulted tax
to 30% and serviceFees
to 10$. Waiting for the rest of the params
The bad way (non compositional)
Let’s build it as an atomic mathematical equation:
const priceCalculator = (
taxPercentage = 0.3,
serviceFees = 10,
price,
discount,
percentCoupon,
valueCoupon,
weight,
$PerKg
) => {
return (
price
- (price * percentCoupon)
- discount
- couponValue
+ (weight * $PerKg)
+ serviceFees
) * (1 + taxPercentage)
}
It does the job. But it has very poor reading, testing, debugging and maintenance experiences.
Let’s do it the good way (composition approach)
Let’s compose…
const priceCalculator = (
taxPercentage = 0.3,
serviceFees = 10,
price,
discount,
percentCoupon,
valueCoupon,
weight,
$PerKg
) => {
const applyTax = (val) => val * (1 + taxPercentage)
const applyServiceFees = (val) => val + serviceFees
const applyPercentCoupon = (val) => val - val * percentCoupon
const applyValueCoupon = (val) => val - valueCoupon
const applyDiscount = (val) => val - discount
const applyShippingCost = (val) => val + weight * $PerKgreturn pipe(
applyPercentCoupon,
applyDiscount,
applyValueCoupon,
applyShippingCost,
applyServiceFees,
applyTax
)(price)
}
This looks so much cleaner, more testable, debuggable and maintainable. All due to the modular mindset we’re adapting to.
We split each operation to be living on its own, then we pipe
them and apply price
to the piped functions.
Give it a try
priceCalculator(10) // NaN
We got a NaN
and that’s unexpected, right?! How can we fix this?
Let’s debug first
Let me introduce a very useful utility to debug chains (pipe
and compose
); inspect
function. It’s very simple. It only logs what it gets, and returns it.
const inspect = (tag) => (x) => {
console.log(`${tag}: ${x}`)
return x
}
Now let’s add that to our chain of functions
const priceCalculator = (
taxPercentage = 0.3,
serviceFees = 10,
price,
discount,
percentCoupon,
valueCoupon,
weight,
$PerKg
) => {
const applyTax = (val) => val * (1 + taxPercentage)
const applyServiceFees = (val) => val + serviceFees
const applyPercentCoupon = (val) => val - val * percentCoupon
const applyValueCoupon = (val) => val - valueCoupon
const applyDiscount = (val) => val - discount
const applyShippingCost = (val) => val + weight * $PerKgreturn pipe(
inspect('price'),
applyPercentCoupon,
inspect('after applyPercentCoupon'),
applyDiscount,
inspect('after applyDiscount'),
applyValueCoupon,
inspect('after applyValueCoupon'),
applyShippingCost,
inspect('after applyShippingCost'),
applyServiceFees,
inspect('after applyServiceFees'),
applyTax
)(price)
}
The results would look something like
priceCalculator(10)
// price: undefined
// after applyPercentCoupon: NaN
//...
Oh! The price
is undefined
, haha that’s because the price
is the 3rd parameter, and we’re passing it first instead!
Let’s fix it very quickly
I’ll just let it accept a single object {}
instead of multiple, like:
const priceCalculator = ({
taxPercentage = 0.3,
serviceFees = 10,
price,
discount,
percentCoupon,
valueCoupon,
weight,
$PerKg
}) => {...}
And we use it like this
priceCalculator({ price: 10 })
// price: 10
// after applyPercentCoupon: NaN
//...
We still get NaN
, but this time for a different reason. Because percentCoupon
is being used while it’s undefined
too.
So let’s fix it by defaulting all parameters (except price
)
const priceCalculator = ({
taxPercentage = 0.3,
serviceFees = 10,
price,
discount = 0,
percentCoupon = 0,
valueCoupon = 0,
weight = 0,
$PerKg = 0
}) => {...}
So now if we use it again, we’ll get a result…
priceCalculator({ price: 10 }) // 26
That’s how composition allows us to test and fix our code easier and quicker, just by inspecting the areas of the pipeline that we suspect.
In Real Life
The problem we debugged was very simple (for a reason). When we go on bigger scale modules, things go nastier quickly and harder to inspect when we use big atomic functions. Splitting functions into smaller ones makes them easier to debug, test, maintain, and develop functions.
Why Composition
Composition is about composing smaller modules into bigger ones. Where we think in a modular way (that’s enforced by composition), we’ll enhance:
- Modularity mindset
- Testability
- Debuggability
- Maintainability
Conclusion
Composition is a way of building big modules out of smaller ones, it makes our code more modular. Thus it’s easier to debug, test, maintain, reuse and even more fun to develop functions. Instead of the old way of jamming all the code into one single area.
Thanks a lot for taking time reading through this article ❤️ I’m cooking the next ones in the series. Please let me know what you think in the comments about this article or the series.
This is an article in a series of articles talking about Functional Programming
In this series of articles we cover the main concepts of Functional Programming. At the end of the series, you’ll be able to tackle problems in a more functional approach.
This series discusses:
0. A Brief Comparison Between Programming Paradigms
- First Class functions
- Pure functions
- Currying
- Composition (this article)
- Functors
- Monads