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!