In the genetic laboratory of Jurassic Park, dinosaur DNA goes through a series of processing stages - extraction, purification, sequencing, validation. Each stage is a separate operation, and chaining them together produces the complete process. This is exactly how function composition works - combining simple functions into more complex operations.
Function composition involves combining two or more functions where the output of one becomes the input of the next. Mathematically:
f(g(x)) - first we apply g, then f.1// Individual operations on dinosaur data
2const normalize = (name) => name.trim().toLowerCase();
3const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
4const addPrefix = (name) => 'DINO-' + name;
5
6// Without composition - nested calls (hard to read)
7const result = addPrefix(capitalize(normalize(' rex ')));
8// 'DINO-Rex'
9
10// With composition - readable pipeline
11const formatDinoName = (name) => addPrefix(capitalize(normalize(name)));
12formatDinoName(' rex '); // 'DINO-Rex'compose chains functions from right to left - the last function in the list is executed first. This mirrors the mathematical notation f(g(x)).1function compose(...fns) {
2 return (value) => fns.reduceRight(
3 (acc, fn) => fn(acc),
4 value
5 );
6}
7
8// Functions processing a DNA sample
9const extractDNA = (sample) => ({ ...sample, dna: sample.tissue + '-DNA' });
10const sequenceDNA = (sample) => ({ ...sample, sequence: sample.dna + '-SEQ' });
11const validateDNA = (sample) => ({ ...sample, valid: sample.sequence.length > 5 });
12
13// Compose: right to left (extractDNA -> sequenceDNA -> validateDNA)
14const processSample = compose(validateDNA, sequenceDNA, extractDNA);
15
16const result = processSample({ tissue: 'TREX-001' });
17// { tissue: 'TREX-001', dna: 'TREX-001-DNA', sequence: 'TREX-001-DNA-SEQ', valid: true }pipe is the inverse of compose - it chains functions from left to right. It reads like a natural data flow, which many programmers find more intuitive.1function pipe(...fns) {
2 return (value) => fns.reduce(
3 (acc, fn) => fn(acc),
4 value
5 );
6}
7
8// Pipe: left to right (natural reading direction)
9const processSample = pipe(extractDNA, sequenceDNA, validateDNA);
10// Identical result, but more readable order
11
12// Practical example - processing park data
13const processReport = pipe(
14 (data) => data.filter((d) => d.status === 'active'),
15 (data) => data.map((d) => ({ name: d.name, zone: d.zone })),
16 (data) => data.sort((a, b) => a.name.localeCompare(b.name)),
17);
18
19const parkData = [
20 { name: 'Rex', zone: 'A', status: 'active' },
21 { name: 'Stego', zone: 'B', status: 'inactive' },
22 { name: 'Blue', zone: 'A', status: 'active' },
23];
24processReport(parkData);
25// [{ name: 'Blue', zone: 'A' }, { name: 'Rex', zone: 'A' }]Both functions do the same thing, but in reverse order. The choice depends on preference:
1// compose: right to left (like in mathematics)
2const process1 = compose(validate, transform, parse);
3// Read as: validate(transform(parse(x)))
4
5// pipe: left to right (like a natural flow)
6const process2 = pipe(parse, transform, validate);
7// Read as: x -> parse -> transform -> validateIn practice,
pipe is used more often because it mirrors the natural direction of data processing - from input to output, from top to bottom.