Programming as Transformations of Data
Functional programming is about transformations. Transformations of immutable data. Let's look at this example:
const execute = () => {
readInput();
processData();
generateOutput();
}
This looks good but each method assumes a lot of things.
readInput
puts data in a variable shared by all the methods.
processData
assumes that the input has already been read.
If you forget to call readInput
before processData
, things will break at runtime. Same goes for generateOutput
, which assumes that data was processed before it was called. Bring concurrency into the mix and things will fall apart pretty quickly.
Let's try to improve it:
const execute = () => {
const input = readInput();
const result = processData(input);
const output = generateOutput(result);
}
The functions here don't make any assumptions. They just get data as parameters and they work only on those parameters without assuming anything else. Each of those are small programs that work independently and execute
is just another program that plugs them together.
But there are a lot of unnecessary intermediate variables. Let's see what happens when we remove them:
const execute = () => generateOutput(processData(readInput()));
This is like Arabic now, we have to read it from right to left. It's easy to fix this issue by defining a pipe
function that takes a value, and passes it to another function. Unix pipes are like water pipes. They take values (or water) and pass them to another programs (or to a sink, a bucket, or to any other location).
const pipe = (v,f) => f(v);
const execute = () => pipe(pipe(readInput(), processData), generateOutput);
This time you read it from left to right, but the core of the program is still hidden in the multitude of pipe
calls. If we can change the pipe to the symbol |
and if we could use infix notation while calling it (e.g. pipe(a,b) = a
pipeb = a | b
) (Javascript unfortunately doesn't allow us to do that), it will convert to:
const execute = () => readInput() | processData | generateOutput;
This time, it doesn't assume state, doesn't have intermediate variables, reads beautifully, conveys the gist of the program elegantly and more importantly, is an expression. readInput
, processData
and generateOutput
are all small programs that can work independently. The execute
function just composes them together. This builds on the Unix philosophy of having small composable programs. Since they share no state, there are no concurrency issues here as well. It's easy to unit test these functions because they are different units – they are not bound to other units via state. You don't need mocks/stubs to test them. You get all of this for free if you just forgo the state
BTW, we can get close to what we have above if we move the pipe to Object prototype:
Object.prototype.pipe = function(f) { return f(this); };
const execute = () => readInput().pipe(processData).pipe(generateOutput);
Very nice post, Abdulsattar.