Functional libraries like Ramda.js are great, and give us some very powerful, useful, simple functionality. But they’re the kind of thing you might not know you need, unless you know you need them.
I’ve been reading (well, okay, _devouring) Eric Elliott’s Composing Software book (and before that, the series of blog posts). Powerful read, easy to understand, lot of meat under that skin. But it’s easy to get lost in there. Trying to understand both the what and why of function composition (and later, object composition) can be challenging.
So I wanted to break it down into a step-by-step, and see if we can make more sense of this as an “evolution of the idea.”
Defining the Problem
Let’s take an absurd example, reversing a string. It’s a lesson we see in all sorts of introductory lessons, and the steps are pretty easy to follow:
- turn the string into an array of letters,
- reverse the array of letters,
- rejoin the reversed array back into a string,
- return the reversed (transformed) string.
Easy to follow, and easy to write. A perfect introduction to methods of core objects in javascript.
Step 1
const reverseString = function reverseString(string){
const stringArray = string.split('');
const reversedStringArray = stringArray.reverse();
const joinedReversedStringArray = reversedStringArray.join('');
return joinedReversedStringArray;
}
So we take each step of the problem, and do that thing. Each step is assigned to a variable because we can, and that variable is passed to the next step as its input. Easy to read, but kind of wasteful.
Wasteful why? Because of method chaining. When we call String.prototype.split()
, that returns an array, and we can chain directly onto that. The Array.prototype.reverse()
acts on an array and modifies it in place, returning the same array, and Array.prototype.join()
returns a string, which we are returning. So we can call each of those methods on their returned result, without needing the intermediary variables
Step 2
const reverseString = function reverseString(string){
return string.split('').reverse().join('');
}
And that does all four steps in one line. Beauty! Note the order of the functions being called there — we split
the string, we reverse
the array, we join
the array.
It is much shorter, and it reads very well. This is often the solution that we as mentors in online programming courses might point to as the cleanest and easiest solution, and it really works. And it does get us closer to where I want us to be.
But this? This is about functional composition. We’ve got a ways to go yet, but we’re closer. Let’s look at another way of doing much the same thing, see if that helps.
Pre-Step 3
While chaining is a great way to go, in terms of readability, it doesn’t really compose well. We can’t build with chained methods like Lego blocks, snapping them together and rearranging as we like. To do that, we need to consider another way of passing data from one function to another.
The pattern of what we’re about to do, in a mathematical sense, might look more like this:
// given functions f, g, and h, and a data point x:
return f( g( h( x ) ) )
We are taking value x
, pushing it into function h
(getting “the h
of x
”), and then taking the returned value from that and pushing it into g
(getting “the g
of h
of x
”), and then taking the returned evaluation from that and pushing it into f
(getting “the f
of g
of h
of x
”).
It makes sense, but it hurts to think in f
and g
and h
and x
hurt my little button-head. Let’s make it a bit more concrete.
/***
* for reference, this was the mathematical idea:
*
* return f(
* g(
* h(
* x
* )
* )
* );
***/
// and the concrete example:
return join(
reverse(
split(
string
)
)
);
So that is doing the same thing - it gets the "split of string
", passes that to get "reverse of (split of string
), then passes that out to get "join of reverse of split of string
." Sounds silly worded that way, but it's part of the mental model. Our function is composed of these steps, in this order.
Step 3
// some utility functions, curried.
const splitOn = (splitString) =>
(original) =>
original.split(splitString);
const joinWith = (joinString) =>
(original) =>
original.join(joinString);
const reverse = (array) => [...array].reverse();
const reverseString = (string) => {
// partially-applied instances of our utilities
const split = splitOn('');
const join = joinWith('')
return join(
reverse(
split(
string
)
)
);
}
There is quite a bit more meat to this one, and it will require some explanation to grok fully what is going on.
First, before we do the reverseString
, we want to turn those Array
or String
methods into composable functions. We’ll make some curried functions, because who doesn’t like abstraction?
-
splitOn
is an abstract wrapper for theString.prototype.split
method, taking as its first parameter the string on which we’ll split. -
joinWith
is an abstract wrapper for theArray.protoype.join
method, taking as its first parameter the string we’ll use for our join. -
reverse
doesn’t take any parameters, but it turnsArray.prototype.reverse
into a composable function in itself.
Now, within our reverseString
, the first step is to partially apply those two abstract functions. We tell split
that it is a reference to splitOn('')
, we tell join
that it is a reference to join.with('')
, and then we have all the parts we need to combine three functions into one call.
This is much better, as we can now see each function, and the order in which they are applied. But this reads a little bit differently than the original chained example. That one read in left-to-right order:
// given string, call split, then call reverse, then call join
return string.split('').reverse().join('');
In functional circles, this is considered “pipe” order. The term comes from the Unix/Linux world, and leads down a whole ‘nother rabbit hole.
Our latest code, rather than reading left-to-right, is processed inside-to-outside:
return join(
reverse(
split(
string
)
)
);
So if we read these in that same left-to-right order, join
, reverse
, split
, we execute them exactly backwards of that. This would be considered “composed” order, and now we’re about to venture into composed-function-land!
Pre Step 4
This is where things start to get fun. First thing to remember is this: functions in javascript are just another kind of data (and thanks, Dan Abramov for the mental models from JustJavascript!). In javascript, we can pass ’em, we can store ’em in arrays or objects, we can manipulate them in fun and exciting ways… and we can combine ’em. And that’s just what we’ll do.
In this iteration, we are going to place all our functions in an array, and then we will simply ask that array to perform each function in turn on a given piece of data. The concept is easy to understand, but again — concrete examples are helpful.
Step 4
// again, the helpers:
const splitOn = (splitString) =>
(original) =>
original.split(splitString);
const joinWith= (joinString) =>
(original) =>
original.join(joinString);
const reverse = (array) => [...array].reverse();
// with those, we can write this:
const reverseString = (string) => {
const instructions = [
splitOn(''),
reverse,
joinWith('')
];
// let's define our transforming variable
let workingValue = string;
for(let i=0; i<instructions.length; i++){
// apply each function and transform our data.
workingValue = instructions[i](workingValue)
}
return workingValue;
}
This is nicely abstracted — inside the reverseString
, we simply create an array of instructions and then we process each one, passing the most recently transformed data in.
If that sounds like a sneaky way of saying we are reducing the array of instructions, you’re either paying attention or reading ahead. 😉
That is exactly where we are going. We are taking an array of instructions, using workingValue
as the starting “accumulator” of that array, and reducing the workingValue
to the final evaluation of each of those instructions, applying the workingValue
each time. This is precisely what Array.prototype.reduce
is for, and it works a treat. Let’s go there next!
Step 5
// I'll leave those helper methods as written.
// Imagine we've placed them in a util library.
import { splitOn, reverse, joinWith } from './utils/util.js';
const reverseString = (string) =>{
const instructions = [
splitOn(''),
reverse,
joinWith('')
];
return instructions.reduce(
(workingValue, instruction) => instruction(workingValue),
// and use string as the initial workingValue
string
)
}
Here, we’ve taken that imperative for
loop and made it a declarative reduce
statement. We simply tell javascript "reduce the original workingValue
by applying each instruction
to it in turn." It is a much more structured way to code, and if we want, we can always add, alter, rearrange the instructions
without breaking the way that reduce
function call works. It simply sees instructions, and does instructions. Is a beautiful thing.
But it would be a colossal pain to have to write each function that way. The concept will be much the same any time we want to combine a number of functions — we write the instructions, then we transform some datapoint based on those instructions. Sounds like another candidate for abstraction.
Pre Step 6
Given that we’re working with the instructions in first-to-last order, we’ll talk about writing a pipe
function first. It’s an easy step from that to reduce, and in terms of how we think, pipe order may make more sense.
So what we want is a function that takes an array of functions, and applies them to a particular data point. Internally, we know it’ll be a reducer, but how might that look?
const pipe = (...arrayOfInstructions) =>
(value) =>
arrayOfInstructions.reduce(
(workingValue, instruction)=>instruction(workingValue), value
);
// or, with shorter variable names:
const pipe = (...fns) => (x) => fns.reduce( (acc, fn)=>fn(acc), x)
Those two are exactly the same — the first simply has longer variable names to make it easier to see what’s happening.
So we’ve made a curryable function here. By partially applying it (passing in any number of functions), we get back a function that wants a value. When we give it a value, it will apply each of the instructions to that value in turn, transforming the value as it goes along. Each time, the latest iteration of that transformed value will be used for the next step, until we reach the end and return the final transformation.
How might that help us? Remember, we want returnString
to be a function that takes a value. And we want to give it a series of instructions. So how’s this look?
// again with our utility functions:
import { splitOn, reverse, joinWith } from './utils/util.js';
import { pipe } from './utils/pipe';
const reverseString = pipe(
splitOn(''),
reverse,
joinWith('')
);
So we call in our utility functions, and we call in our pipe
function, and then we’re ready to begin. We partially apply the instructions to pipe
, which returns a function expecting a value — which is exactly what we want reverseString
to be! Now, when we call reverseString
with a string argument, it uses that string as the final argument to the reducer, runs each of those instructions, and gives us a return result!
Look closely, though: our reverseString
is a function, defined without a body! I can't stress enough, this is weird. This is not what we're accustomed to when we write functions. We expect to write a function body, to arrange some instructions, to do some stuff - but that is all happening for us. The pipe
function takes all the function references passed in above, and then returns a function... awaiting a value. We aren't writing a reverseString
function, we're sitting in the pipe
function's closure!
Remember above when I explained that we can look at pipe
as similar to chained order? If you read the above pipe
call, you can read it in the same order. But when we compose, it is the reverse of pipe — while we might read it left-to-right (or outermost to innermost), it should process from right to left. Let’s write a compose
function, and compare it to pipe
.
// remember,this is our pipe function in the compact terms
const pipe = (...fns) =>
(x) =>
fns.reduce( (acc, fn) => fn(acc), x);
// compose is eerily similar - we just want to reverse-order
const compose = (...fns) =>
(x) =>
fns.reduceRight( (acc, fn) => fn(acc), x);
If you look at those two functions, the only difference between them is that pipe
uses fns.reduce()
while compose
uses fns.reduceRight()
. Otherwise, nothing has changed. We could test them easily, if we wanted:
import { splitOn, reverse, joinWith } from './utils/util.js';
import { pipe, compose } from './utils/my_fp_lib.js';
const pipedReverseString = pipe(
splitOn(''),
reverse,
joinWith('')
);
const composedReverseString = compose(
joinWith(''),
reverse,
splitOn('')
);
// let's use them!
console.log(
pipedReverseString('Hello World')===composedReverseString('Hello World')
);
// logs true
Note that this is hardly the best explanation or implementation of pipe and reduce. There are far better, more robust FP libraries out there doing a far better job of implementing this. But what I’m doing here is more about explaining the how of it, for some who might be intimidated by the whole idea of functional composition. It doesn’t have to be intimidating, really. When we break it down to smaller steps, we can see that we already know most of this — it is simply how we combine that knowledge together.
And when I wrote something similar to this some time back, the biggest critique I got was “what’s the point? I’m not gaining anything by writing little functions for every little detail!” There is some truth to that, for the person who made the comment. For me, having that compose
functionality means that my more complex functions become testable and debuggable quickly and easily, my development becomes more about what I want to do and less about how I’ll do it, my thinking becomes more abstract.
For example, suppose we wanted to add some inline debugging to the pipe version of our reverseString
function? We could easily add that, without breaking anything:
import {splitOn, reverse, joinWith} from './utils/util.js';
import { pipe } from './utils/my_fp_lib.js';
// this would be a candidate for a useful function to add to util.js
const trace = (message) => {
(value) => console.log(message, value);
return value;
}
const reverseString = pipe(
trace('Starting Value'),
splitOn(''),
trace('After split'),
reverse,
trace('After reverse'),
joinWith('')
);
console.log(
reverseString('Hello World')
);
/***
* logs out
* Starting Value Hello World
*
* After split [
* 'H', 'e', 'l', 'l',
* 'o', ' ', 'W', 'o',
* 'r', 'l', 'd'
* ]
*
* After reverse [
* 'd', 'l', 'r', 'o',
* 'W', ' ', 'o', 'l',
* 'l', 'e', 'H'
* ]
*
* dlroW olleH
***/
The only thing we’ve changed here is that we’ve added a trace
function, something we couldn’t do with a chained function call or a normal nested series of functions. This is one of the secret superpowers of composition — we can combine things easily that might not be easy or obvious otherwise.
Edit: There was a typo in the trace
function - we want that to return
the value, so we continue to pass it up (or down) the pipe
(or compose
).
Recap
I hope this helped clear up some, for those (like me) who were initially confused looking at Eric’s compose
and pipe
functions. Not because they were poorly written at all, simply because I was still thinking in a linear style and these functions are next-level.
I wanted to take us from the 101-level javascript, and start looking at how we might easily take the knowledge we already have and turn it into something more. First, by exploring two different ways of doing the same things — chained methods or nested function calls. Each does similar things, but the mindset and reading-order behind both are a bit different. Both are equally valid, and both apply to functional composition.
If you got these concepts, you’re already well on your way down the functional programming rabbit-hole. Welcome to the madhouse, have a hat! If you didn’t quite get the concepts yet, it’s not a failing — these are deep and twisty applications of ideas. You get a hat anyway!
Top comments (7)
Ok, functional composition makes sense. Especially when there are parts which have single responsibility.
This concept is over 50 years old as base level routines have been joined in ways to ensure proper formulas always return the same results when the input is the same.
The only change has been the terminology used.
Eric Elliot is a person I refer to as a Functionista, a militant functional programming purveyor.
In SOLID, all refactored code winds up functional in nature when regiously following the single responsibility pattern.
In the end Composition is king. Smart programming favors it.
I've read and reread this a number of times, and I honestly can't make heads or tails of it. You do raise some very valid points:
And then things get subjective. Your comment about Eric seems to me much like those I'd heard from a fellow magician, back when I was a family performer. He would watch other magicians doing well, having great stage or closeup shows, and he couldn't celebrate their success - he had to pigeonhole them to define what he perceived as their limits.
You have your opinion of the man, and you're entitled to it. I've never met him, only read blog posts and watched some podcasts. But the way he approaches this subject resonated with me, and if I get the chance, I'll thank him for it.
Composition is not king. OO is not king. These are tools, different tools which have different applications in a mature, rounded developer's tool box. Religiously following any paradigm is a great way to block out the benefits of any other.
If I've misread any of this comment, I do apologize. You have some very valid points, and you're welcome to your opinions. While I agree to the benefits of much of what it seems you believe, I don't and won't hold them as dogma.
Composition is king because it allows for individual small part changes without affecting other parts.
If we stick to SRP
then each part (or function as some call it) will not do anything outside its responsibility. This makes even functional composition
bullet proof.
I never said OO is king but neither is the Functional programming style, contrary to Elliot.
Some prescriptions should be followed religiously; such as DRY and SRP, which are; in essence, the same thing. Just say no to monolithic code, it's unmaintainable.
Just like continual refactoring to achieve SRP winding up as a function or method (same meaning) doing only one thing the OOP way winds up as being functional in the end.
The point I'm making is; most OOP bashing is done by Javascript adopters who never knew OOP. They don't like to talk about their own use of inheritance of the React or Angular classes. If they do admit the niceties of inheritance in React or Angular they'll always qualify it by saying 'it's shallow inheritance' which is purported to be the only way it's good.
Finally my dislike of Elliot was in calling him out on his TypeScript hatred. His reasons were only his bias, meaning he could care less to how it helped others.
He then later disliked the integration of the Class and other TypeScript inventions into Javascript for no good reason other than his boat was rocked.
In fairness,
class
in javascript is unnecessary and misleading, but that's my own personal bias.I've been coding since the early 80s, I was OOP for a long while when it was the new and shiny on desktops. And I see benefits to that pattern, on occasion. OOP in the sense of SmallTalk, where the intent is very directed and meaningful.
But I don't see OOP and FP in opposition. I see them in balance. Used properly, both tools have a very valuable place.
I do appreciate your perspectives, and thank you for clarifying your issues with him. I can see why you'd feel that way.
Thank you for taking the time, and really looking forward to future conversations!
Likewise Toby.
When I started college in 1981 we only had basic and assembly language.
My first job after school was working on a Navy shipboard computer where everything was in assembly.
When IBM standardized the desktop, assembly was still there. It was Microsoft who first used "c" to write gui programs. Finally adding in C++ later.
I actually worked at IBM in 1987 on a midrange using assembly language.
I distinctly remember people saying things like 'we don't need 4th generation languages, we have assembly". Today we almost always use 4th generation languages and rarely, if ever use assembly.
So it's from that perspective that I find those that reject new features in their own language to be just like the 'we don't need it group'. They are stuck in their own self inflicted technical debt gap.
But what really surprises me are the so called experts in their domain shunning new features.
Elliott still to this day pans TypeScript which had features not introduced to Javascript until many years later. He is to me a 'we don't need it' person.
great write Toby!
I have been interested in functional JavaScript and functional programming in general in a large part because of you
right now I am learning about TDD and testing in general, and it looks like it's a great time to try and apply functional tricks and concepts in my next project
the challenge: write at least half of the functions without writing body at all, just with pipes and compositions
Quite a lot of functions are simply compositions of others. I'd be very interested in seeing how it works for you!