Finally, we have arrived at function composition.
In functional programming, we want everything to be a function. We especially want unary functions if possible. If we can convert all functions to unary functions, then magical things can happen.
Manipulating functions and their acceptable number of inputs can be extremely expressive. In this section, we will explore how to compose new functions from smaller functions: little units of logic that combine into whole programs that are greater than the sum of the functions on their own.
Composing functions allows us to build complex functions from many simple, generic functions. By treating functions as building blocks for other functions, we can build truly modular applications with excellent readability and maintainability.
Before we define the compose()
polyfill, you can see how it all works with these following examples:
var roundedSqrt = Math.round.compose(Math.sqrt) console.log( roundedSqrt(5) ); // Returns: 2 var squaredDate = roundedSqrt.compose(Date.parse) console.log( squaredDate("January 1, 2014") ); // Returns: 1178370
In math, the composition of the f
and g
variables is defined as f(g(x))
. In JavaScript, this can be written as:
var compose = function(f, g) { return function(x) { return f(g(x)); }; };
But if we left it at that, we would lose track of the this
keyword, among other problems. The solution is to use the apply()
and call()
utilities. Compared to curry, the compose()
polyfill is quite simple.
Function.prototype.compose = function(prevFunc) { var nextFunc = this; return function() { return nextFunc.call(this,prevFunc.apply(this,arguments)); } }
To show how it's used, let's build a completely contrived example, as follows:
function function1(a){return a + ' 1';} function function2(b){return b + ' 2';} function function3(c){return c + ' 3';} var composition = function3.compose(function2).compose(function1); console.log( composition('count') ); // returns 'count 1 2 3'
Did you notice that the function3
parameter was applied first? This is very important. Functions are applied from right to left.
Because many people like to read things from the left to the right, it might make sense to apply the functions in that order too. We'll call this a sequence instead of a composition.
To reverse the order, all we need to do is swap the nextFunc
and prevFunc
parameters.
Function.prototype.sequence = function(prevFunc) { var nextFunc = this; return function() { return prevFunc.call(this,nextFunc.apply(this,arguments)); } }
This allows us to now call the functions in a more natural order.
var sequences = function1.sequence(function2).sequence(function3); console.log( sequences('count') ); // returns 'count 1 2 3'
Here are five different implementations of the same floorSqrt()
functional composition. They seem to be identical, but they deserve scrutiny.
function floorSqrt1(num) { var sqrtNum = Math.sqrt(num); var floorSqrt = Math.floor(sqrtNum); var stringNum = String(floorSqrt); return stringNum; } function floorSqrt2(num) { return String(Math.floor(Math.sqrt(num))); } function floorSqrt3(num) { return [num].map(Math.sqrt).map(Math.floor).toString(); } var floorSqrt4 = String.compose(Math.floor).compose(Math.sqrt); var floorSqrt5 = Math.sqrt.sequence(Math.floor).sequence(String); // all functions can be called like this: floorSqrt<N>(17); // Returns: 4
But there are a few key differences we should go over:
To say that less code is better is missing the point. Code is more maintainable when the effective instructions are more concise. If you reduce the number of characters on the screen without changing the effective instructions carried out, this has the complete opposite effect—code becomes harder to understand, and decidedly less maintainable; for example, when we use nested ternary operators, or we chain several commands together on a single line. These approaches reduce the amount of 'code on the screen', but they don't reduce the number of steps actually being specified by that code. So the effect is to obfuscate and make the code harder to understand. The kind of conciseness that makes code easier to maintain is that which effectively reduces the specified instructions (for example, by using a simpler algorithm that accomplishes the same result with fewer and/or simpler steps), or when we simply replace code with a message, for instance, invoking a third-party library with a well-documented API.
map
function. This works fairly well, but it is not mathematically correct.compose()
function in action. All methods are forced to be unary, pure functions that encourage the use of better, simpler, and smaller functions that do one thing and do it well.compose()
function in reverse sequence, which is just as valid.The most important aspect of compose is that, aside from the first function that is applied, it works best with pure, unary functions: functions that take only one argument.
The output of the first function that is applied is sent to the next function. This means that the function must accept what the previous function passed to it. This is the main influence behind type signatures.
Type Signatures are used to explicitly declare what types of input the function accepts and what type it outputs. They were first used by Haskell, which actually used them in the function definitions to be used by the compiler. But, in JavaScript, we just put them in a code comment. They look something like this: foo :: arg1 -> argN -> output
Examples:
// getStringLength :: String -> Intfunction getStringLength(s){return s.length}; // concatDates :: Date -> Date -> [Date]function concatDates(d1,d2){return [d1, d2]}; // pureFunc :: (int -> Bool) -> [int] -> [int]pureFunc(func, arr){return arr.filter(func)}
In order to truly reap the benefits of compose, any application will need a hefty collection of unary, pure functions. These are the building blocks that are composed into larger functions that, in turn, are used to make applications that are very modular, reliable, and maintainable.
Let's go through an example. First we'll need many building-block functions. Some of them build upon the others as follows:
// stringToArray :: String -> [Char] function stringToArray(s) { return s.split(''); } // arrayToString :: [Char] -> String function arrayToString(a) { return a.join(''); } // nextChar :: Char -> Char function nextChar(c) { return String.fromCharCode(c.charCodeAt(0) + 1); } // previousChar :: Char -> Char function previousChar(c) { return String.fromCharCode(c.charCodeAt(0)-1); } // higherColorHex :: Char -> Char function higherColorHex(c) {return c >= 'f' ? 'f' : c == '9' ? 'a' : nextChar(c)} // lowerColorHex :: Char -> Char function lowerColorHex(c) { return c <= '0' ? '0' : c == 'a' ? '9' : previousChar(c); } // raiseColorHexes :: String -> String function raiseColorHexes(arr) { return arr.map(higherColorHex); } // lowerColorHexes :: String -> String function lowerColorHexes(arr) { return arr.map(lowerColorHex); }
Now let's compose some of them together.
var lighterColor = arrayToString .compose(raiseColorHexes) .compose(stringToArray) var darkerColor = arrayToString .compose(lowerColorHexes) .compose(stringToArray) console.log( lighterColor('af0189') ); // Returns: 'bf129a' console.log( darkerColor('af0189') ); // Returns: '9e0078'
We can even use compose()
and curry()
functions together. In fact, they work very well together. Let's forge together the curry example with our compose example. First we'll need our helper functions from before.
// component2hex :: Ints -> Int function componentToHex(c) { var hex = c.toString(16); return hex.length == 1 ? "0" + hex : hex; } // nums2hex :: Ints* -> Int function nums2hex() { return Array.prototype.map.call(arguments, componentToHex).join(''); }
First we need to make the curried and partial-applied functions, then we can compose them to our other composed functions.
var lighterColors = lighterColor.compose(nums2hex); var darkerRed = darkerColor .compose(nums2hex.partialApply(255)); Var lighterRgb2hex = lighterColor .compose(nums2hex.partialApply()); console.log( lighterColors(123, 0, 22) ); // Returns: 8cff11 console.log( darkerRed(123, 0) ); // Returns: ee6a00 console.log( lighterRgb2hex(123,200,100) ); // Returns: 8cd975
There we have it! The functions read really well and make a lot of sense. We were forced to begin with little functions that just did one thing. Then we were able to put together functions with more utility.
Let's look at one last example. Here's a function that lightens an RBG value by a variable amount. Then we can use composition to create new functions from it.
// lighterColorNumSteps :: string -> num -> string function lighterColorNumSteps(color, n) { for (var i = 0; i < n; i++) { color = lighterColor(color); } return color; } // now we can create functions like this: var lighterRedNumSteps = lighterColorNumSteps.curry().compose(reds)(0,0); // and use them like this: console.log( lighterRedNumSteps(5) ); // Return: 'ff5555' console.log( lighterRedNumSteps(2) ); // Return: 'ff2222'
In the same way, we could easily create more functions for creating lighter and darker blues, greens, grays, purples, anything you want. This is a really great way to construct an API.
We just barely scratched the surface of what function composition can do. What compose does is take control away from JavaScript. Normally JavaScript will evaluate left to right, but now the interpreter is saying "OK, something else is going to take care of this, I'll just move on to the next." And now the compose()
function has control over the evaluation sequence!
This is how Lazy.js
, Bacon.js
and others have been able to implement things such as lazy evaluation and infinite sequences. Up next, we'll look into how those libraries are used.