Decomposing Composition

parenttobias

Toby Parent

Posted on December 16, 2021

Decomposing Composition

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:

  1. turn the string into an array of letters,
  2. reverse the array of letters,
  3. rejoin the reversed array back into a string,
  4. 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;
}
Enter fullscreen mode Exit fullscreen mode

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('');
}
Enter fullscreen mode Exit fullscreen mode

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 ) ) )
Enter fullscreen mode Exit fullscreen mode

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  
            )
          )
       );
Enter fullscreen mode Exit fullscreen mode

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
             )
           )
         );
}
Enter fullscreen mode Exit fullscreen mode

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 the String.prototype.split method, taking as its first parameter the string on which we’ll split.
  • joinWith is an abstract wrapper for the Array.protoype.join method, taking as its first parameter the string we’ll use for our join.
  • reverse doesn’t take any parameters, but it turns Array.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('');
Enter fullscreen mode Exit fullscreen mode

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  
    )  
  )  
);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
  )
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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('')
);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
 ***/
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
parenttobias
Toby Parent

Posted on December 16, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Javascript: Functional Composition
javascript Javascript: Functional Composition

September 5, 2022

Question Mark (?) Operators ASAP
javascript Question Mark (?) Operators ASAP

January 24, 2022

Decomposing Composition
javascript Decomposing Composition

December 16, 2021