*This post is my first post in my explained series - where I take a concept from software engineering and explain it as succinctly and simply as possible. The goal is to have three sections for each explanation - 1. What is it? 2. Example 3. When would I use it?.
The goal is to help you increase your breadth of knowledge whilst still giving you some level of depth.
I would love your feedback if you have any on what other topics you’d like to see - you can reach me on twitter: @harryblucas*
What is it?
This is another one of those terms that sounds fancy, but really isn’t.
Function composition is process of combining two or more functions together into a single function, that produces the same result as if the functions were called independently and the result was passed from one to another.
To break it down - we take two (or more) functions, and pass the result of calling the first function with the supplied argument to the second function and so forth.
1const func3 = (value) => func2(func1(value))
To simplify it, you can think of a function as an individual Lego block.
Function composition is then the process of combining one or more of these Lego blocks in to a larger structure.
For an example, let’s pretend we want to take a string, and count how many words start with the letter
First we create our individual Lego blocks (functions) that we are going to use to build the bigger block (function).
Which in code would be defined as:
1const split = (string) => string.split(" ")23const downCase = (list) => list.map((s) => s.toLowerCase())45const countS = (list) => list.reduce((count, word) => word === 's' ? count + 1 : count, 0)
Since these blocks are all the same shapes and have matching side - we can combine them together 💪
To do this in code:
1const wordsStartWithS = (string) => countS(downcase(split(string)))
To read a function composed like this, you read it inside out.
However this isn’t as clear as it could be, so instead to make it a bit clearer, what a lot of functional programmers will do is introduce a
pipe function, which is super simple and makes the above, even clearer. To read an example of what a pipe function is you can click here.
1const pipe = (...fns) => (initValue) => fns.reduce((val, func) => func(val), initValue)
Then we would use it like this:
1const wordsStartWithS = (string) => pipe(2 split,3 downcase,4 countS5)(string)
Which is now a very readable function declaration that reads like a recipe when cooking (split then downcase then count), all without a variable assignment.
Again just to reiterate what we’ve done here is combine multiple, smaller functions, into a single larger function.
Now of course this isn’t the most robust example, but it is a start for getting your head around exactly what function composition is.
When should I use it?
You can use function composition to compose your entire application together. You can take your smallest functions and combine them together into larger business cases, you can then combine these functions together with other business cases to build a use case - you get the picture - it’s just functions all the way down.
The great thing about adopting more of a function composition mindset is that it forces you to break apart your functions into their smallest component pieces.
This promotes composability, reuse and is in my opinion much easier to read.
For example, if we wrote this the traditional way:
1const wordsStartWithS = (string) => string.split(" ").map((s) => s.toLowerCase).reduce((count, word) => word === "s" ? count + 1 : count, 0)
Whilst we have gained the benefit of having it all in one line, we have:
- Made it harder to read (lots of extra noise)
- Harder for the reader to grok intent (they have to parse every single method call to figure out what it’s doing)
- Removed any possibility of reusing those component parts (which leads to duplicate code)