Expert · Functional patterns and testing · topic 48 of 61 · 25 min
Composition, currying and pipelines
Explanation
Small functions, combined
Instead of one big function that cleans, validates, converts and formats a value, write four tiny functions and connect them. Each is easy to test and reuse. Composition is the act of connecting them so that one function's output becomes the next function's input.
const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const collapseSpaces = (s) => s.replace(/\s+/g, ' ');
const cleanName = (s) => collapseSpaces(lower(trim(s))); // read inside-out: hard
console.log(cleanName(' ASHA RAO ')); // 'asha rao'pipe and compose
pipe takes functions and returns a new function that runs them left to right. compose does the same right to left (the mathematical order). pipe usually reads more naturally.
const pipe = (...fns) => (input) => fns.reduce((value, fn) => fn(value), input);
const compose = (...fns) => (input) => fns.reduceRight((value, fn) => fn(value), input);
const cleanName2 = pipe(trim, lower, collapseSpaces);
console.log(cleanName2(' ASHA RAO ')); // 'asha rao'Currying
A curried function takes its arguments one at a time, returning a new function after each, until it has them all. It is named after the mathematician Haskell Curry. Currying makes it easy to create specialised versions of general functions.
const addGst = (rate) => (amount) => Math.round(amount * (1 + rate / 100));
const gst5 = addGst(5);
const gst18 = addGst(18);
console.log(gst5(200), gst18(200)); // 210 236
console.log([100, 250].map(gst18)); // [118, 295]Partial application
Partial application fixes some arguments of a normal function and returns a function waiting for the rest. bind can do it, or a small helper.
function discount(percent, max, price) {
return price - Math.min(price * percent / 100, max);
}
const festival = discount.bind(null, 20, 500); // percent 20, max 500 fixed
const partial = (fn, ...fixed) => (...rest) => fn(...fixed, ...rest);
const member = partial(discount, 5, 200);
console.log(festival(1000), festival(5000), member(1000)); // 800 4500 950Putting it together: a pricing pipeline
const applyCoupon = (pct) => (p) => p * (1 - pct / 100);
const addShipping = (fee, freeAbove) => (p) => (p >= freeAbove ? p : p + fee);
const roundRupee = (p) => Math.round(p);
const finalPrice = pipe(applyCoupon(10), addShipping(40, 499), roundRupee);
console.log(finalPrice(450), finalPrice(600)); // 445 540Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Composition, currying and pipelines · 1/6
In this lesson you will combine small functions into bigger ones.
Prefer a video by a teacher? Find free videos on “javascript function composition pipe currying partial application” on YouTube ↗
In real life: Cleaning customer records before import
A CRM import cleans thousands of phone numbers from Excel sheets: remove spaces and dashes, strip a leading +91 or 0, and check the length. Each rule is a tiny function, and the import runs every number through a pipe of them.
const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
const digitsOnly = (s) => s.replace(/[^\d+]/g, '');
const stripPrefix = (s) => s.replace(/^(\+91|0)/, '');
const valid = (s) => (/^[6-9]\d{9}$/.test(s) ? s : null);
const cleanPhone = pipe(digitsOnly, stripPrefix, valid);
console.log(['+91 98765-43210', '098765 43210', '12345'].map(cleanPhone));
// ['9876543210', '9876543210', null]Practice (0/3 done)
Write pipe(...fns) that returns a function running fns from left to right on its input.
Write a curried function between(min) that returns (max) => (n) => true when min <= n <= max. Then use it to build isValidAge = between(18)(60).
Build a pricing function finalPrice with pipe from three curried steps: applyCoupon(pct) (subtract pct percent), addShipping(fee, freeAbove) (add fee unless the price is already >= freeAbove), and roundRupee. finalPrice = pipe(applyCoupon(10), addShipping(40, 499), roundRupee).
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Memoization, debounce and throttleFunctional patterns and testing · same module
- Testing your codeFunctional patterns and testing · same module
- Classic coding questionsInterview preparation and career
- Reading text: grep, less, pipes and redirectionLinux and Red Hat (RHCSA to RHCE) · Linux and the shell
- Bash scripting, text tools and cronDevOps: Git, Docker, Kubernetes and CI/CD · Linux for DevOps
- Inheritance and compositionPython Programming · Object-oriented programming
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
pipe(f, g)(x) equals…
Answer: g(f(x))
pipe runs left to right: f first, then g.
const add = (a) => (b) => a + b; What is add(2)(3)?
Answer: 5
add(2) returns b => 2 + b; calling it with 3 gives 5.
What does partial application do?
Answer: Fixes some arguments and returns a function for the rest
It pre-fills some arguments.
Why should each step in a pipe take one argument?
Answer: Each step receives only the previous step's single return value
A function returns one value, which becomes the next input.
In a curry helper, what does fn.length give?Related
Answer: The number of declared parameters
Function.length is the count of declared parameters (before any default or rest).
In "sort names.txt | uniq -c", what does the pipe do?Related
Answer: Sends sort's output to uniq -c as input
A pipe connects stdout of the left command to stdin of the right one.
Which relationship suits composition best?Related
Answer: A Car has an Engine
"Has-a" relationships are modelled by holding the part inside.
Which pipeline lists the 5 most frequent client IPs, assuming the IP is the first column of access.log?Related
Answer: awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -n 5
awk picks the IP, sort groups equal lines, uniq -c counts them, sort -rn puts the biggest first.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 3 more topics in “Functional patterns and testing” before its exam: Memoization, debounce and throttle, Testing your code, Test doubles and testable design.
- Module 10 exam: Functional patterns and testing · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Performance and Node.js. 61 topics left in the course before the final exam.