Skip to content

Instantly share code, notes, and snippets.

@ripter
Created May 25, 2012 05:49
Show Gist options
  • Save ripter/2786033 to your computer and use it in GitHub Desktop.
Save ripter/2786033 to your computer and use it in GitHub Desktop.
Compose Method, Functional JavaScript

#What is a compose method? Compose takes a series of single parameter functions that are used as the parameters for the previous functions. The last function can take any number of parameters.

Code example:

function text(a) {
    return 'item: ' + a;
}
function hello(a) {
    return 'Hello ' + a;
}

var c = compose(text, hello);
c('Chris');  // outputs: 'item: Hello Chris'

// so c looks something like:
function c(a) {
    return text(hello(a));
}

##What's the point? It's not hard to pass parameters of one function to another, so why do we need the compose method? I'm going to steal an example from DrBoolean.

Say we want to know if the next number is going to be odd.

function nextNumber(num) {
    return num + 1;
}

function isOdd(num) {
    return !!(num % 2);
}

//Typical JavaScript call
isOdd(nextNumber(7));  // outputs: false
isOdd(nextNumber(8));  // outputs: true

// using composition
var isNextOdd = compose(isOdd, nextNumber);

isNextOdd(7);  // outputs: false
isNextOdd(8);  // outputs: true

So you can see isNextOdd(7) is a lot easier to type and read than isOdd(nextNumber(7)).

###So it's like a partial? It is like a partial in the fact that it completes part of the argument list, but unlike a partial it evaluates the parameters at call time.

##Let's write a compose method! Let's start my figuring out what our method should look like. We will want more than two arguments to make this easier to understand, so we will compose a method that tells us if the number after next is odd.

// for compose(isOdd, nextNumber, nextNumber)(7)
function isNumberAfterNextOdd(7) {
    return isOdd.apply(isOdd, 
        [nextNumber.apply(nextNumber, 
            [nextNumber.apply(nextNumber, 
                [7])
            ]
        )
    ]);
}

// abstract it a little further
function isNumberAfterNextOdd() {
    var args = Array.prototype.slice.call(arguments);

    return isOdd.apply(isOdd, [nextNumber.apply(nextNumber, [nextNumber.apply(nextNumber, args)])]);
}

// lets take out the nested calls and start backwards
function isNumberAfterNextOdd() {
    var args = Array.prototype.slice.call(arguments);

    var calls = nextNumber.apply(nextNumber, args);
    calls = nextNumber.apply(nextNumber, [calls]);
    calls = isOdd.apply(isOdd, [calls]);

    return calls;
}

When we take out the deeply nested list, it becomes easier to understand what we need.

function compose() {
    var fns = Array.prototype.slice.call(arguments)
        ;

    return function() {
        var args = Array.prototype.slice.call(arguments)
            , fn = fns[fns.length-1]
            , i = fns.length - 1 // -1 because the last function is special
            , result = null
            ;

        result = fn.apply(fn, args);
        while (i--) {
            fn = fns[i];
            result = fn.apply(fn, [result]);
        }
        
        return result;
    }
}

// which lets us do
var isNumberAfterNextOdd = compose(isOdd, nextNumber, nextNumber);

isNumberAfterNextOdd(7);  // outputs: true
isNumberAfterNextOdd(8);  // outputs: false

If you are like me, then this is all well and good, but we can do better.

##Golfing the method I'm perfectly happy to use underscore as a dependency. It has a lot of great functional methods that we can use. In fact it even has _.compose() which does exactly what we want.

But I want to continue improving our version just for fun. We will use their _.reduceRight() to get rid of the loop.

function compose() {
    var fns = Array.prototype.slice.call(arguments)
        ;

    return function() {
        var args = Array.prototype.slice.call(arguments)
            ;

        return _.reduceRight(fns, function(args, fn) {
            return fn.apply(fn, [].concat(args));
        }, args);
    }
}

// Basic golfing
function compose(){var slice=Array.prototype.slice,fns=slice.call(arguments);return function(){var args=slice.call(arguments);return _.reduceRight(fns,function(args,fn){return fn.apply(fn,[].concat(args));},args);}}

This was way to large to tweet, which is disappointing. There might be a way to make this 140 characters or less.

##Conclusion Function composition is a great tool to have. Thankfully underscore already provides _.compose() ready to use. Combined with partials (`_.bind()') you can start righting some very nice functional code in JavaScript.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment