Functional Programming: functional composition
- tagged:
- programming
- javascript
- fp
Creating functions like Lego blocks
Functional composition
Imagine you have some series of computations to perform:
function yell(str) { const upper = str.toUpperCase() const exclaim = `${upper}!` return exclaim}// let's also imagine this is some non-trivial function
What if instead of a list of instructions for the computer, telling it to create two throw-away variables so that we can have a final value, we instead just write a sequence of actions?
yell = upper exclaim
This, is the core idea of our function above, it describes what we want, rather than how to do it.
These separate functions don't actually exist, so let's create them:
const upper = str => str.toUpperCase()const exclaim = str => `${str}!`
Now, instead of the original function, we can say:
const yell = str => upper(exclaim(str))
There's still too much noise here for my liking. We can do better with a higher-order function compose
:
function compose(f, g) { return function (x) { return f(g(x)) }}// orconst compose = (f, g) => x => f(g(x))
This lets us write
const yell = str => compose(upper, exclaim)(str)
But wrapping that in an anonymous function is unnecessary. This is actually identical to:
const yell = compose(upper, exclaim)
This expresses the idea much more concisely, and is much closer to our original idea that yell = upper exclaim
.
Compare the following:
const yell = str => { const upper = str.toUpperCase() const exclaim = `${upper}!` return exclaim}yell('hello world')// vs.const upper = str => str.toUpperCase()const exclaim = str => `${str}!`const yell = compose(exclaim, upper)yell('hello world')
Not only is it shorter, but I find the signal-to-noise ratio is lower, and we have much more re-usable and composable code.
It might not be immediately obvious, but the order of evaluation in our compose function is right-to-left. If we look at both examples:
f(g(x)) === compose(f, g)(x)
g(x)
will be the first thing evaluated, and its value will then be passed as the argument to f()
.
When reading composed functions, it's important to remember that the input will first be given to the function on the right, and the return value will be from the function on the left.
This creates an annoying context shift for me, since usually we read left-to-right, top-to-bottom, fortunately, we can make a function that does left-to-right composition:
const pipe = (g, f) => x => f(g(x))
If it helps put things back in perspective:
f(g(x)) === compose(f, g)(x) === pipe(g, f)(x)
The benefits of these really shine with variadic implementations, so you can pass as many functions to be composed or piped as you like:
One immediate benefit of using composed functions can be seen when used with Array#map
.
If you've ever done
someList.map(e => someFn(e)).map(e => someOtherFn(e))
You're hopefully aware that this will iterate over the array twice, one for each .map()
.
Using pipe
we can cleanly do this in a single iteration:
someList.map(pipe(someFn, someOtherFn))
And since pipe is just a reverse compose
, we can extend our knowledge of map
such that:
l.map(f).map(g) === l.map(x => g(f(x))) === l.map(compose(g, f))