multimethod.js

multimethod.js 0.1.0

(c) 2011 Kris Jordan

multimethod is freely distributable under the MIT license. For details and documentation: http://krisjordan.com/multimethod-js

(function() {

Multimethods are a functional programming control structure for dispatching function calls with user-defined criteria that can be changed at run time. Inspired by clojure's multimethods, multimethod.js provides an alternative to classical, prototype-chain based polymorphism.

Internal Utility Functions

No operation function used by default by default.

    var noop = function() {};

Identity dispatch function. Default value of dispatch.

    var identity = function(a) { return a; };

A method in multimethod is a (match value, function) pair stored in an array. indexOf takes a value and array of methods and returns the index of the method whose value is equal to the first argument. If no match is found, false is returned.

    var indexOf = function(value, methods) {
        for(var i in methods) {
            var matches  = methods[i][0];
            if(_(value).isEqual(matches)) {
                return i;
            }
        }
        return false;
    }

Given a dispatch value and array of methods, return the function of the method whose match value corresponds to a dispatch value.

    var match = function(value, methods) {
        var index = indexOf(value, methods);
        if(index !== false) {
            return methods[index][1];
        } else {
            return false;
        }
    }

Simple, consistent helper that returns a native value or invokes a function and returns its return value. Used by when and default allowing short-hand notation for returning values rather than calling functions.

    var toValue = function(subject, args) {
        if(_.isFunction(subject)) {
            return subject.apply(this, args);
        } else {
            return subject;
        }
    };

Plucking a single property value from an object in dispatch is commonly used. The internal pluck function returns a function suitable for use by dispatch for just that purpose.

    var pluck = function(property) {
        return function(object) {
            return object[property];
        }
    };

Implementation

multimethod is a higher-order function that returns a closure with methods to control its behavior.

    var multimethod = function(dispatch) { 

Private Properties

_dispatch holds either a dispatch function or a string corresponding to the property name whose value will be plucked and used as the dispatch criteria.

        var _dispatch,

_methods is a an array of method arrays. A method is [ matchValue, implementation ].

            _methods   = [],

_default is the fallback method when a multimethod is called and matches no other method.

            _default   = noop;

The fundamental control flow of the multimethod is implemented in _lookup. First we invoke the dispatch function, this gives us our match criteria. Then we match a method based on the criteria or return the default method.

        var _lookup    = function() {
            var criteria    = _dispatch.apply(this, arguments),
                method      = match(criteria, _methods);
            if(method !== false) {
                return method;
            } else {
                return _default;
            }
        };

The result of calling multimethod's "factory" function is this function.

        var returnFn  = function() {
            var method = _lookup.apply(this, arguments);
            return toValue.call(this, method, arguments);
        };

Member Methods / API

dispatch is the accessor to the multimethod's _dispatch function. When called with a string we create an anonymous pluck function as a shorthand.

        returnFn['dispatch'] = function(dispatch) {
            if(_.isFunction(dispatch)) {
                _dispatch = dispatch;
            } else if(_.isString(dispatch)) {
                _dispatch = pluck(dispatch);
            } else {
                throw "dispatch requires a function or a string.";
            }
            return this;
        }

If multimethod is called/"constructed" with a dispatch value we go ahead and set it up here. Otherwise dispatch is the identity function.

        returnFn.dispatch(dispatch || identity);

when introduces new methods to a multimethod. If the matchValue has already been registered the new method will overwrite the old method.

        returnFn['when'] = function(matchValue, fn) {
            var index = indexOf(matchValue, _methods);
            if(index !== false) {
                _methods[index] = [matchValue, fn];
            } else {
                _methods.push([matchValue, fn]);
            }
            return this;
        }

remove will unregister a method based on matchValue

        returnFn['remove'] = function(matchValue) {
            var index = indexOf(matchValue, _methods);
            if(index !== false) {
                _methods.splice(index, 1);
            }
            return this;
        }

default is an accessor to control the _default, fallback method that is called when no match is found when the multimethod is invoked and dispatched.

        returnFn['default'] = function(method) {
            _default = method;
            return this;
        }

Our multimethod instance/closure is fully setup now, return!

        return returnFn;
    };

The following snippet courtesy of underscore.js. Export multimethod to the window/exports namespace.

    if (typeof exports !== 'undefined') {
        if (typeof module !== 'undefined' && module.exports) {
            exports = module.exports = multimethod;
            var _ = require('underscore');
        }
        exports.multimethod = multimethod;
    } else if (typeof define === 'function' && define.amd) {
        define('multimethod', function() {
            return multimethod;
        });
    } else {
        this['multimethod'] = multimethod;
        var _ = this['_'];
    }

    multimethod.version = '0.1.0';

}).call(this);