Functional composition
https://blog.bitsrc.io/functional-programming-composition-2e9b863d8bcb
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 🧱
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 * $PerKg;
return 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 * $PerKg;
return 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!