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.curry()); 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.