├── History.md ├── Readme.md ├── core.js ├── package.json └── tests ├── basic.js └── test-all.js /History.md: -------------------------------------------------------------------------------- 1 | # Changes # 2 | 3 | ## 0.0.1 / 2011-10-10 ## 4 | 5 | - Initial release 6 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # dispatcher # 2 | 3 | [Pattern matching] for JavaScript. 4 | 5 | Library can be used to implement functions supporting range of different 6 | dispatchers, in a declarative manner avoiding mess of conditional blocks & 7 | manual argument validations. While primarily this aims to improve code 8 | readability and maintainability, in some cases this can be useful to optimize 9 | specific, hot code paths. 10 | 11 | 12 | ## Examples ## 13 | 14 | ```js 15 | 16 | var dispatcher = require('dispatcher/core').dispatcher 17 | 18 | function number(value) { 19 | if (typeof(value) !== 'number') throw Error('Not a number') 20 | return value 21 | } 22 | 23 | var sum = dispatcher({ doc: "sums given numbers", added: "0.1.0" }, 24 | // If no arguments are passed then 0. 25 | [], function() { return 0 }, 26 | // If only one argument is passed return back. 27 | [ number ], function(x) { return x }, 28 | // Optimize two argument case. 29 | [ number, number ], function(x, y) { return x + y }, 30 | // If more then two then take all the args starting from second as 31 | // rest array. 32 | [ number, [] ], function (x, rest) { 33 | called.number_rest ++; 34 | return rest.reduce(function(x, y) { 35 | return x + y 36 | }, x) 37 | }) 38 | 39 | 40 | sum() // => 0 41 | sum(1) // => 1 42 | sum(2, 3) // => 5 43 | sum(2, 5, 17, 1) // => 25 44 | sum([ 4 ]) // TypeError -> Unsupported dispatcher 45 | 46 | 47 | // Define guards 48 | function string(value) { 49 | if (typeof(value) !== 'string') throw Error('Not a string') 50 | return value 51 | } 52 | function lambda(value) { 53 | if (typeof(value) !== 'function') throw Error('Not a function') 54 | return value 55 | } 56 | function array(value) { 57 | if (!Array.isArray(value)) throw Error('Not an array') 58 | return value 59 | } 60 | function object(value) { 61 | if (!value || typeof(value) !== 'object') throw new Error('Not an object') 62 | return value 63 | } 64 | 65 | var map = dispatcher( 66 | // Function that operates on strings 67 | [ lambda, string ], function(lambda, string) { 68 | var index = -1, length = string.length, chars = [] 69 | while (++index < length) chars[index] = lambda(string[index]) 70 | return chars.join('') 71 | }, 72 | // Function that operates on arrays 73 | [ lambda, array ], function(lambda, array) { 74 | var index = -1, length = array.length, elements = [] 75 | while (++index < length) elements[index] = lambda(array[index]) 76 | return elements 77 | }, 78 | // Function that operates on objects 79 | [ lambda, object ], function(lambda, object) { 80 | var pair, value = Object.create(Object.getPrototypeOf(object)) 81 | for (var key in object) { 82 | pair = lambda(key, object[key]) 83 | value[pair[0]] = pair[1] 84 | } 85 | return value 86 | }, 87 | // Function that operates on everything else 88 | [ lambda, , ], function(lambda, value) { 89 | return lambda(value) 90 | }) 91 | 92 | 93 | map(function($) { return $.toUpperCase() }, 'hello world') 94 | // => 'HELLO WORLD' 95 | 96 | map(function($) { return $ * 2 }, [ 1, 2, 3 ]) 97 | // => [ 2, 4, 6 ] 98 | 99 | map(function(k, v) { return [ '@' + k, v + '!' ] }, { foo: 1, bar: 'baz' }) 100 | // => { '@foo': '1!', '@bar': 'baz!' } 101 | 102 | map(function($) { return $ + 1 }, 6) 103 | // => 7 104 | 105 | map(function($) { return $ + '!' }, 'hello', 'world') 106 | // TypeError -> Unsupported dispatcher 107 | ``` 108 | 109 | ## Install ## 110 | 111 | npm install dispatcher 112 | 113 | [Pattern matching]:http://en.wikipedia.org/wiki/Pattern_matching 114 | -------------------------------------------------------------------------------- /core.js: -------------------------------------------------------------------------------- 1 | /* vim:set ts=2 sw=2 sts=2 expandtab */ 2 | /*jshint asi: true undef: true es5: true node: true devel: true 3 | forin: true latedef: false supernew: true */ 4 | /*global define: true */ 5 | 6 | !(typeof(define) !== "function" ? function($) { $(typeof(require) !== 'function' ? (function() { throw Error('require unsupported'); }) : require, typeof(exports) === 'undefined' ? this : exports, typeof(module) === 'undefined' ? {} : module); } : define)(function(require, exports, module) { 7 | 8 | "use strict"; 9 | 10 | var dispatcher = (function(slicer, isArray) { 11 | // Creating a shortcuts: 12 | slicer = Array.prototype.slice 13 | isArray = Array.isArray 14 | 15 | function match(pattern, args) { 16 | /** 17 | Utility function takes arguments `pattern` and actual arguments as `args` 18 | array and performs match over them. If given `args` match the `pattern` 19 | returned value is arrays of matches, otherwise it's `null`. 20 | **/ 21 | try { 22 | var length = pattern.length, index = -1, matches = [] 23 | 24 | // If more arguments are passed then pattern expects and pattern does 25 | // not captures rest arguments by special `[]` match return `null` since 26 | // pattern won't match. 27 | if (length < args.length && !isArray(pattern[length - 1])) return null 28 | 29 | while (++index < length) { 30 | // If pattern for argument is undefined (usually specified as hole in 31 | // array via trailing `,`) pattern matches anything, so map it. 32 | if (typeof(pattern[index]) === 'undefined') 33 | matches[index] = args[index] 34 | 35 | // If pattern for the argument is a function then it's a guard that 36 | // either extracts data or throws if it does not matches. Map extracted 37 | // data or return `null` in exception handler. 38 | else if (typeof(pattern[index]) === 'function') 39 | matches[index] = pattern[index](args[index]) 40 | 41 | // Empty array as argument pattern, has a special meaning of capturing 42 | // this and all the following arguments into an array. There for slice 43 | // all the `args` starting from this one push into matches and return 44 | // immediately. 45 | else if (isArray(pattern[index])) { 46 | matches[index] = slicer.call(args, index) 47 | if (pattern[index][0]) 48 | matches[index] = matches[index].map(pattern[index][0]) 49 | return matches 50 | } 51 | 52 | // In all other cases patterns are treated as constants. If it matches 53 | // argument, put it into matches. 54 | else if (args[index] === pattern[index]) 55 | matches[index] = args[index] 56 | 57 | // Otherwise match fails and return with `null` immediately. 58 | else 59 | return null 60 | } 61 | return matches 62 | } catch (error) { 63 | // If function pattern does not matches argument it throws, in which 64 | // case match is failed and `null returned. 65 | return null 66 | } 67 | } 68 | 69 | return function dispatcher(meta, params) { 70 | /** 71 | This function can be used to implement functions supporting range of 72 | protocols, in a declarative manner avoiding mess of conditional blocks & 73 | manual argument validations. While primarily this aims to improve code 74 | readability and maintainability, in some cases this can be useful to 75 | optimize specific, hot code paths. 76 | 77 | Function accepts following input: 78 | 79 | - Optionally metadata (name, version, doc) JSON object can be passed 80 | as a first argument. 81 | - Optionally `doc` string may be passed as first argument, in which case 82 | metadata object will be created with this doc string. 83 | - All the other arguments represent input patterns and associated 84 | functions (as following argument) to be invoked in case of match. 85 | 86 | Returned function, when invoked will find matching pattern for the passed 87 | arguments and will delegate to an associated function. If no match is found 88 | `TypeError` with `"Unsupported protocol"` message is thrown. 89 | 90 | Pattern represents an array. Each element is an argument pattern. Usually 91 | that's a function that either extracts data (or just returns given data) on 92 | match or throws if does not match. Empty array as last pattern has a special 93 | meaning of extracting rest arguments. `undefined` or typically hole in array 94 | matches anything. Anything else is matches argument only if it's equal. 95 | **/ 96 | 97 | // Array containing pattern and associated function paths. 98 | var routes = [] 99 | 100 | // Take all the params except 1st one, as we expect it to be a metadata 101 | // object associated with a resulting function. 102 | params = slicer.call(arguments, 1) 103 | 104 | // If metadata is a string, then we interpret it as documentation string 105 | // and there for normalize to a metadata object with a doc property 106 | // containing this string. 107 | if (typeof(meta) === 'string') meta = { doc: meta } 108 | 109 | // If first argument is not an array or is non-object, then it's part of 110 | // pattern match rules so we unshift it back to params. 111 | if (isArray(meta)) (params.unshift(meta), meta = {}) 112 | 113 | meta.error = meta.error || 'Unsupported protocol' 114 | 115 | // `params` are expected to have a following form: 116 | // `[ pattern1, fn1, pattern2, fn2, ... ]` where `pattern` is an array 117 | // representing an arguments signature for the following `fn` function. 118 | // We walk through parameters in order to collect all routes consisting of 119 | // (optional) pattern and associated function. 120 | 121 | var length = params.length, index = 0, route 122 | while (index < length) 123 | routes.push({ pattern: params[index++], fn: params[index++] }) 124 | 125 | function router() { 126 | /** 127 | Composite function that implements multiple different protocols for 128 | interacting with different input. 129 | **/ 130 | 131 | var length = routes.length, index = 0, route, args 132 | 133 | // Try to find a route, that has a pattern matching given arguments. If 134 | // such rout is found, extract match from arguments and delegate to the 135 | // associated function of the route. 136 | while (index < length) { 137 | route = routes[index++] 138 | args = match(route.pattern, arguments) 139 | if (args) return route.fn.apply(this, args) 140 | } 141 | 142 | // If matching route not found throw an exception. 143 | throw TypeError(meta.error) 144 | } 145 | 146 | // Metadata is copied to the composed function (`name` property is treated 147 | // specially, it's copied as `displayName` property so that debuggers can 148 | // provide more meaningful information for such function). 149 | router.meta = meta 150 | if (router.name) router.displayName = meta.name 151 | 152 | return router 153 | } 154 | })() 155 | exports.dispatcher = dispatcher 156 | 157 | }); 158 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dispatcher", 3 | "id": "dispatcher", 4 | "version": "0.0.2", 5 | "description": "Pattern matching for JavaScript.", 6 | "keywords": [ "functions", "pattern", "match", "cotract", "dispatch" ], 7 | "author": "Irakli Gozalishvili (http://jeditoolkit.com)", 8 | "homepage": "https://github.com/Gozala/dispatcher", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Gozala/dispatcher.git", 12 | "web": "https://github.com/Gozala/dispatcher" 13 | }, 14 | "bugs": { 15 | "url": "http://github.com/Gozala/dispatcher/issues/" 16 | }, 17 | "devDependencies": { 18 | "test": ">=0.0.10" 19 | }, 20 | "engines": { 21 | "node": ">=0.4.x" 22 | }, 23 | "scripts": { 24 | "test": "node tests/test-all.js" 25 | }, 26 | "licenses": [{ 27 | "type" : "MIT", 28 | "url" : "http://jeditoolkit.com/LICENSE" 29 | }] 30 | } 31 | -------------------------------------------------------------------------------- /tests/basic.js: -------------------------------------------------------------------------------- 1 | /* vim:set ts=2 sw=2 sts=2 expandtab */ 2 | /*jshint asi: true newcap: true undef: true es5: true node: true devel: true 3 | forin: true */ 4 | /*global define: true */ 5 | 6 | (typeof define === "undefined" ? function ($) { $(require, exports, module) } : define)(function (require, exports, module, undefined) { 7 | 8 | "use strict"; 9 | 10 | var dispatcher = require('../core').dispatcher 11 | 12 | exports['test arity based match'] = function(assert) { 13 | function identity(value) { return function() { return value } } 14 | 15 | function number(value) { 16 | if (typeof(value) !== 'number') throw Error('Not a number') 17 | return value 18 | } 19 | 20 | var called = { 21 | nothing: 0, 22 | number: 0, 23 | number_number: 0, 24 | number_rest: 0 25 | } 26 | 27 | // Match different arity. 28 | var sum = dispatcher({ doc: "sums given numbers", added: "0.1.0" }, 29 | [], function() { called.nothing ++; return 0 }, 30 | [ number ], function(x) { called.number ++; return x }, 31 | [ number, number ], function(x, y) { called.number_number ++; return x + y }, 32 | [ number, [] ], function (x, rest) { 33 | called.number_rest ++; 34 | return rest.reduce(function(x, y) { 35 | return x + y 36 | }, x) 37 | }) 38 | 39 | assert.equal(sum(), 0, 'sum() -> 0') 40 | assert.equal(called.nothing, 1, 'called sum with no args once') 41 | assert.equal(sum(2), 2, 'sum(2) -> 2') 42 | assert.equal(sum(17), 17, 'sum(17) -> 17') 43 | assert.equal(called.number, 2, 'called sum with one arg twice') 44 | assert.equal(sum(2, 3), 5, 'sum(2, 3) -> 5') 45 | assert.equal(called.number_number, 1, 'called sum with two args once') 46 | assert.equal(sum(2, 5, 17, 1), 25, 'sum(2, 5, 17, 1) -> 25') 47 | assert.equal(called.number_rest, 1, 'called sum with rest once') 48 | 49 | assert.throws(function() { 50 | sum({}) 51 | }, /Unsupported protocol/, 'throws on unexpected input type') 52 | } 53 | 54 | exports['test argument type based match'] = function(assert) { 55 | function string(value) { 56 | if (typeof(value) !== 'string') throw Error('Not a string') 57 | return value 58 | } 59 | function lambda(value) { 60 | if (typeof(value) !== 'function') throw Error('Not a function') 61 | return value 62 | } 63 | function array(value) { 64 | if (!Array.isArray(value)) throw Error('Not an array') 65 | return value 66 | } 67 | function object(value) { 68 | if (!value || typeof(value) !== 'object') throw new Error('Not an object') 69 | return value 70 | } 71 | 72 | // Match by different input types. 73 | var map = dispatcher({ added: "0.1.0" }, 74 | [ lambda, string ], function(lambda, string) { 75 | var index = -1, length = string.length, chars = [] 76 | while (++index < length) chars[index] = lambda(string[index]) 77 | return chars.join('') 78 | }, 79 | [ lambda, array ], function(lambda, array) { 80 | var index = -1, length = array.length, elements = [] 81 | while (++index < length) elements[index] = lambda(array[index]) 82 | return elements 83 | }, 84 | [ lambda, [ array ] ], function(lambda, arrays) { 85 | var length = arrays.length, index, element, value = [], n = 0 86 | while (true) { 87 | element = [] 88 | index = -1 89 | while (++index < length) { 90 | if (arrays[index].length <= n) return value 91 | element[index] = arrays[index][n] 92 | } 93 | value[n++] = lambda.apply(null, element) 94 | } 95 | }, 96 | [ lambda, object ], function(lambda, object) { 97 | var pair, value = Object.create(Object.getPrototypeOf(object)) 98 | for (var key in object) { 99 | pair = lambda(key, object[key]) 100 | value[pair[0]] = pair[1] 101 | } 102 | return value 103 | }, 104 | [ lambda,, ], function(lambda, value) { 105 | return lambda(value) 106 | }) 107 | 108 | assert.equal(map(function($) { return $.toUpperCase() }, 'hello world'), 109 | 'HELLO WORLD', 'works with strings') 110 | assert.deepEqual(map(function($) { return $ * 2 }, [ 1, 2, 3 ]), 111 | [ 2, 4, 6 ], 'works with array') 112 | assert.deepEqual(map(function(k, v) { return [ '@' + k, v + '!' ] }, 113 | { a: 'foo', 'b': 2 }), 114 | { '@a': 'foo!', '@b': '2!' }, 'works with hashes') 115 | assert.equal(map(function($) { return $ + 1 }, 6), 7, 'works with numbers') 116 | 117 | assert.deepEqual(map(function(a, b) { return '' + a + b }, [ 1, 2, 3 ], [ 'a', 'b' ]), 118 | [ '1a', '2b' ], 'rest guard works') 119 | 120 | assert.throws(function() { 121 | map(function() {}, 'hello', 'world') 122 | }, /Unsupported protocol/, 'throws on more arguments then expected') 123 | } 124 | 125 | if (module == require.main) 126 | require("test").run(exports); 127 | 128 | }) 129 | -------------------------------------------------------------------------------- /tests/test-all.js: -------------------------------------------------------------------------------- 1 | /* vim:set ts=2 sw=2 sts=2 expandtab */ 2 | /*jshint asi: true newcap: true undef: true es5: true node: true devel: true 3 | forin: true */ 4 | /*global define: true */ 5 | 6 | (typeof define === "undefined" ? function ($) { $(require, exports, module) } : define)(function (require, exports, module, undefined) { 7 | 8 | "use strict"; 9 | 10 | exports['test basic'] = require('./basic') 11 | 12 | if (module == require.main) 13 | require("test").run(exports); 14 | 15 | }) 16 | --------------------------------------------------------------------------------