Toward modular, reusable code
4.5 Composing function pipelines
4.5.2 Functional composition: separating description from evaluation
In essence, functional composition is a process used to group together complex behavior that has been broken into simpler tasks. I defined it briefly in chapter 1, and now I’ll explain it in detail. Let’s go over a quick example that uses Ramda’s R.compose to combine two pure functions:
const str = `We can only see a short distance ahead but we can see plenty there that needs to be done`;
const explode = (str) => str.split(/\s+/);
const count = (arr) => arr.length;
const countWords = R.compose(count, explode);
countWords(str); //-> 19
Arguably, this code is easy to read, and its meaning easily derived by glancing at the function’s constituent parts. The interesting quality of this program is that evaluation never takes place until countWords is run; in other words, the functions passed by name (explode and count) are dormant within the composition. The result of com-position is another function that waits to be called with its respective argument: the argument to countWords. This is the beauty of function composition: separating a func-tion’s description from its evaluation.
I’ll explain what happens behind the scenes. The call to countWords(str) runs explode with the given sentence and passes its output (array of strings) into count, which computes the length of the array. Composition connects outputs with inputs, creating true function pipelines. Let’s examine a more formal definition. Consider two functions f and g with their respective input and output types:
g :: A -> B f :: B -> C
Figure 4.10 draws a set of arrows connecting all groups. This abstract example shows a function (arrow) f that takes an argument of type B and returns a C. Another function (arrow) g takes an A and returns a B. The composition of g :: A -> B and f :: B -> C,
Splits a sentence into an array of words
Counts the words
g is a function from type A to B.
f is a function from type B to C.
pronounced (“f composed of g”), results in another function (arrow) from A -> C, as shown in figure 4.11. This can be expressed more formally as
f g = f(g) = compose :: (B -> C) -> (A -> B) -> (A -> C)
Recall that with referential transparency, functions are nothing more than arrows con-necting one object of a group to another.
This leads to another important software development principle, which is the backbone of modular systems. Because composition loosely binds type-compatible functions at their boundaries (inputs and outputs), it fairly satisfies the principle of programming to interfaces. In the previous example, you have a function explode ::
String -> [String] composed with the function count :: [String] -> Number; in other words, each function only knows or cares about the next function’s interface and isn’t worried about its implementation. Although it isn’t part of the JavaScript lan-guage, compose can be naturally expressed as a higher-order function.
function compose(/* fns */) { let args = arguments;
let start = args.length - 1;
return function() { let i = start;
let result = args[start].apply(this, arguments);
while (i--)
result = args[i].call(this, result);
return result;
};
}
Listing 4.8 Implementation of compose
A
Figure 4.10 Showing the set of input and output types for functions f and g. Function g maps A values to B values, and function f maps B values to C values. Composition happens because f and g are compatible.
Figure 4.11 The composition of two functions is a new function directly mapping the inputs of the first function to the output of the second. The composition is also a referentially transparent mapping between inputs and outputs.
The output of compose is another function that’s called on actual arguments.
Dynamically applies the function on the arguments passed in
Iteratively invokes the subsequent functions based on the previous return value
Luckily, Ramda provides an implementation of R.compose that you can use so you don’t have to implement this yourself. Let’s write a validation program that checks for a valid SSN (you’ll reuse a lot of these helper functions throughout the book):
const trim = (str) => str.replace(/^\s*|\s*$/g, '');
const normalize = (str) => str.replace(/\-/g, '');
const validLength = (param, str) => str.length === param;
const checkLengthSsn = _.partial(validLength, 9);
From these functions, you can create others:
const cleanInput = R.compose(normalize, trim);
const isValidSsn = R.compose(checkLengthSsn, cleanInput);
cleanInput(' 444-44-4444 '); //-> '444444444' isValidSsn(' 444-44-4444 '); //-> true
Taking this fundamental concept further, as you can see in figure 4.12, entire pro-grams can be built with the combination of simple functions.
This concept isn’t limited to functions; entire programs can be built from the combi-nation of other side effect–free, pure programs or modules. (Based on the earlier def-inition of a function, used throughout this book, I’ll use the terms function, program, and module loosely to refer to any executable unit with inputs and output.)
Removes any spaces before and after the input Removes all dashes
Checks the length of a string Configures the function with parameter 9 to check the length of an SSN (9)
Composes normalize and trim results in the cleanInput function
Figure 4.12 Complex functions can be built by composing simple functions. Just as functions combine, entire programs made from different modules (containing more functions) that can also combine in this fashion.
Composition is a conjunctive operation, which means it joins elements using a logical AND operator. For instance, the function isValidSsn is made from checkLengthSsn and cleanInput. In this manner, programs are derivations of the sum of all their parts.
In chapter 5, we’ll tackle problems that require disjunctive behavior to express condi-tions where funccondi-tions can return one of two results, A OR B.
Alternatively, you can augment JavaScript’s Function prototype to add compose.
Here’s the exact same behavior in a style similar to function chaining from chapter 3:
Function.prototype.compose = R.compose;
const cleanInput = checkLengthSsn.compose(normalize).compose(trim);
If you like this better, feel free to use it. In the next chapter, you’ll learn that this mecha-nism of chaining methods is prevalent in functional algebraic data types called monads.
Personally, I recommend sticking to the more functional form, because it’s much more succinct and flexible and works better in conjunction with functional libraries.