Functional Production Lines (or Composing Functions)
In the early days of their manufacture, a car was built in one place in a factory. One person built the chassis, another person built the body on top of it, and someone else came along and dropped in an engine. The developing car stayed still, and the workers moved to the car. This wasted a lot of time moving workers, and their associated heavy equipment, between locations in the factory. With the development of the assembly line (popularised by, although not really invented by Henry Ford), a paradigm shift was achieved. Rather than move the workers to the car, why not move the car to the workers and stop the time wasted moving them around?
There is a parallel that we can draw between car manufacture and programming, and applying a similar shift in thinking to programming techniques can give us similar gains in productivity.
Let's first view our functions throught the lens of early car manufacture - moving the workers to the car. In this analogy, the workers are functions - they take an input (a partially completed car), perform some work, and return an output (a slightly more completed car). Let's define some workers:
const buildChassis = () => {
return {
wheels: "Four wheels"
};
}
const buildBody = chassis => {
return {
...chassis,
body: {
doors: "Two doors"
}
};
}
const paintCar = chassisWithDoors {
return {
...chassisWithDoors,
body: {
...chassisWithDoors.body,
paint: "A sweet paintjob"
}
}
}
Now we can build a car!
const chassis = buildChassis();
const chassisWithBody = buildBody(chassis);
const completedCar = paintCar(chassisWithBody);
console.log(completedCar);
/*
{
wheels: "Four round wheels",
body: {
doors: "Two doors",
paint: "A sweet paintjob"
}
}
*/
It works - we have a completed car! The process was a bit inefficient though - like our early factory we completed a step, then brought the next function along to do its job. Wouldn't it be better if, like a production line, we could move our arguments to the right function? With function composition we can - we define a build car production line that moves our fledgling car to the right workers. It'll work like this:
const buildCar = compose(
buildChassis,
buildBody,
paintCar
);
const completedCar = buildCar();
Just like a production line, we define the steps that our car needs to take, then start the process and watch it move between the functions. The key point is that the output of each function needs to match the input of the next function. I.e. the output of buildChassis
needs to match the input of buildBody
, and the output of buildBody
needs to match the input of paintCar
.
Now we just needs to define the compose
function above to wire it all together. To push the analogy to its limit (OK, we're well beyond the limit by now!), we can think of compose as the conveyor belt that moves intermediate values (partially completed cars) between functions (workers). Fortunately, we can define compose
very succinctly using rest parameters and a reducer:
const compose = (...fns) => (input) => {
return fns.reduce((output, currentFunction) =>
currentFunction(output)
, input);
}
This does exactly as discussed - the output from the previous function is used as the input to the next function. The input
parameter is used as the seed to the reduce function for occasions when we need to pass an argument to the first function we are composing.
The great thing about compose
is that because it returns normal functions, we can compose composed functions themselves. This allows us to use a divide-and-conquer approach, creating progressively larger pieces of the whole process by composing them together. If we add some more hypothetical steps to our car building process, we could group them into related functions:
const buildChassis = compose(
weldSteelStruts,
addSuspension
);
const addWheels = compose(
addWheelsToCar,
inflateTyres
);
const buildBody = compose(
beatSheetMetal,
weldToCar,
paintBody
);
const buildCar = compose(
buildChassis,
addWheels,
buildBody
);
Just like the car manufacturers, I have found that moving data rather than moving functions has simplified my work process and increased my efficiency. Thinking about programs as gradual compositions of small functions to create a greater whole is a powerful way to think, and one that can bring great advantages. I'll follow this post with something about different function combinators which will truly unlock the power of composition and combination.