Function composition

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.

Note

Unary functions are functions that take only a single input. Functions with multiple inputs are polyadic, but we usually say binary for functions that accept two inputs and ternary for three inputs. Some functions don't accept a specific number of inputs; we call those variadic.

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.

Compose

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.

Sequence – compose in reverse

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'

Compositions versus chains

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:

  • Obviously the first method is verbose and inefficient.
  • The second method is a nice one-liner, but this approach becomes very unreadable after only a few functions are applied.

    Note

    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.

  • The third approach is a chain of array functions, notably the map function. This works fairly well, but it is not mathematically correct.
  • Here's our 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.
  • The last approach uses the compose() function in reverse sequence, which is just as valid.

Programming with compose

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.

Note

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.194.123