From de39f57629ea2d0373b6ad52251ea23718029433 Mon Sep 17 00:00:00 2001 From: Reginald Braithwaite Date: Tue, 14 May 2013 10:07:16 -0400 Subject: [PATCH 1/5] Precursors to Duck Monads post --- test/function.combinators.js | 10 +++++++ underscore.function.arity.js | 46 +++++++++++++++++++++++++++++- underscore.function.combinators.js | 20 +++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/test/function.combinators.js b/test/function.combinators.js index f64aba1..b777efa 100644 --- a/test/function.combinators.js +++ b/test/function.combinators.js @@ -90,6 +90,16 @@ $(document).ready(function() { deepEqual(echo3(1,2,3), [[1], 2, 3], 'should return the arguments provded'); deepEqual(echo3(1,2,3,4), [[1, 2], 3, 4], 'should return the arguments provded'); }); + + test("mapArgsWith", function () { + var echo = _.unsplatl(function (args) { return args; }); + function double (n) { return n * 2; } + function plusOne (n) { return n + 1; } + + deepEqual(_.mapArgsWith(double, echo)(), [], "should handle the empty case") + deepEqual(_.mapArgsWith(double, echo)(42), [84], "should handle one arg") + deepEqual(_.mapArgsWith(plusOne, echo)(1, 2, 3), [2, 3, 4], "should handle many args") + }); test("flip2", function() { var div = function(n, d) { return n/d; }; diff --git a/underscore.function.arity.js b/underscore.function.arity.js index ca70cf6..04e885d 100644 --- a/underscore.function.arity.js +++ b/underscore.function.arity.js @@ -64,8 +64,52 @@ return function quaternary (a, b, c, d) { return fun.call(this, a, b, c, d); }; + }, + + // swiss-army-knife currying and partial application function + // for functions taking two arguments + // + // if you provide an argument, it partially applies it. + // if you do not provide an argument, performs a "greedy" + // curry. + call2: function (fun, optionalFirst, optionalLast) { + if (arguments.length === 3) { + return fun(optionalFirst, optionalLast); + } + else if (arguments.length === 1) { + return function call2 (first, optionalLast) { + if (arguments.length === 1) { + return function (last) { + return fun(first, last); + }; + } + else return fun(first, optionalLast); + }; + } + else return function call2 (last) { + return fun(optionalFirst, last); + }; + }, + + call2flipped: function (fun, optionalLast, optionalFirst) { + if (arguments.length === 3) { + return fun(optionalFirst, optionalLast); + } + else if (arguments.length === 1) { + return function call2flipped (last, optionalFirst) { + if (arguments.length === 1) { + return function (first) { + return fun(first, last); + }; + } + else return fun(optionalFirst, last); + }; + } + else return function call2flipped (first) { + return fun(first, optionalLast); + }; } - + }); _.arity = (function () { diff --git a/underscore.function.combinators.js b/underscore.function.combinators.js index f68d04e..0e33289 100644 --- a/underscore.function.combinators.js +++ b/underscore.function.combinators.js @@ -17,6 +17,17 @@ var truthy = function(x) { return (x !== false) && existy(x); }; var __reverse = [].reverse; var __slice = [].slice; + var __map = [].map; + + // n.b. depends on underscore.function.arity.js + + // Takes a target function and a mapping function. Returns a function + // that applies the mapper to its arguments before evaluating the body. + function baseMapArgs (fun, mapFun) { + return _.arity(fun.length, function () { + return fun.apply(this, __map.call(arguments, mapFun)); + }); + }; // Mixing in the combinator functions // ---------------------------------- @@ -149,6 +160,13 @@ } } }, + + // map the arguments of a function + mapArgs: _.curry2(baseMapArgs), + + // map the arguments of a function, takes the mapping function + // first so it can be used as a combinator + mapArgsWith: _.curry2flipped(baseMapArgs), // Returns a function that returns an array of the calls to each // given function for some arguments. @@ -208,5 +226,7 @@ }); _.unsplatr = _.unsplat; + + })(this); From 491a1c040949b2aa524d9d363e3d2b138ab05338 Mon Sep 17 00:00:00 2001 From: Reginald Braithwaite Date: Tue, 14 May 2013 10:11:18 -0400 Subject: [PATCH 2/5] Oops, somehow failed to commit this change! --- underscore.function.combinators.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/underscore.function.combinators.js b/underscore.function.combinators.js index 0e33289..42ba96c 100644 --- a/underscore.function.combinators.js +++ b/underscore.function.combinators.js @@ -162,11 +162,11 @@ }, // map the arguments of a function - mapArgs: _.curry2(baseMapArgs), + mapArgs: _.call2(baseMapArgs), // map the arguments of a function, takes the mapping function // first so it can be used as a combinator - mapArgsWith: _.curry2flipped(baseMapArgs), + mapArgsWith: _.call2flipped(baseMapArgs), // Returns a function that returns an array of the calls to each // given function for some arguments. From dbc8ef5680065c2da2b05cefe2e2751136601800 Mon Sep 17 00:00:00 2001 From: Reginald Braithwaite Date: Tue, 14 May 2013 13:48:25 -0400 Subject: [PATCH 3/5] Updated to do just one thing --- test/function.arity.js | 21 +++++++++++ underscore.function.arity.js | 56 ++++++++++-------------------- underscore.function.combinators.js | 4 +-- 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/test/function.arity.js b/test/function.arity.js index 6735f47..006a4e1 100644 --- a/test/function.arity.js +++ b/test/function.arity.js @@ -51,4 +51,25 @@ $(document).ready(function() { equal(flipWithArity(echo).length, echo.length, "flipWithArity gets its arity correct"); }); + + test("curry2", function () { + + function echo () { return [].slice.call(arguments, 0); } + + deepEqual(echo(1, 2), [1, 2], "Control test"); + deepEqual(_.curry2(echo)(1, 2), [1, 2], "Accepts arguments greedily"); + deepEqual(_.curry2(echo)(1)(2), [1, 2], "Accepts curried arguments"); + + }); + + test("curry2flipped", function () { + + function echo () { return [].slice.call(arguments, 0); } + + deepEqual(_.flip2(echo)(1, 2), [2, 1], "Control test"); + deepEqual(_.curry2flipped(echo)(1, 2), [2, 1], "Accepts arguments greedily"); + deepEqual(_.curry2flipped(echo)(1)(2), [2, 1], "Accepts curried arguments"); + + }); + }); diff --git a/underscore.function.arity.js b/underscore.function.arity.js index 04e885d..ecefe91 100644 --- a/underscore.function.arity.js +++ b/underscore.function.arity.js @@ -66,47 +66,27 @@ }; }, - // swiss-army-knife currying and partial application function - // for functions taking two arguments - // - // if you provide an argument, it partially applies it. - // if you do not provide an argument, performs a "greedy" - // curry. - call2: function (fun, optionalFirst, optionalLast) { - if (arguments.length === 3) { - return fun(optionalFirst, optionalLast); - } - else if (arguments.length === 1) { - return function call2 (first, optionalLast) { - if (arguments.length === 1) { - return function (last) { - return fun(first, last); - }; - } - else return fun(first, optionalLast); - }; - } - else return function call2 (last) { - return fun(optionalFirst, last); + // greedy currying for functions taking two arguments. + curry2: function (fun) { + return function curried (first, optionalLast) { + if (arguments.length === 1) { + return function (last) { + return fun(first, last); + }; + } + else return fun(first, optionalLast); }; }, - call2flipped: function (fun, optionalLast, optionalFirst) { - if (arguments.length === 3) { - return fun(optionalFirst, optionalLast); - } - else if (arguments.length === 1) { - return function call2flipped (last, optionalFirst) { - if (arguments.length === 1) { - return function (first) { - return fun(first, last); - }; - } - else return fun(optionalFirst, last); - }; - } - else return function call2flipped (first) { - return fun(first, optionalLast); + // greedy flipped currying for functions taking two arguments. + curry2flipped: function (fun) { + return function curried (last, optionalFirst) { + if (arguments.length === 1) { + return function (first) { + return fun(first, last); + }; + } + else return fun(optionalFirst, last); }; } diff --git a/underscore.function.combinators.js b/underscore.function.combinators.js index 42ba96c..0e33289 100644 --- a/underscore.function.combinators.js +++ b/underscore.function.combinators.js @@ -162,11 +162,11 @@ }, // map the arguments of a function - mapArgs: _.call2(baseMapArgs), + mapArgs: _.curry2(baseMapArgs), // map the arguments of a function, takes the mapping function // first so it can be used as a combinator - mapArgsWith: _.call2flipped(baseMapArgs), + mapArgsWith: _.curry2flipped(baseMapArgs), // Returns a function that returns an array of the calls to each // given function for some arguments. From e907a37021d6cc770273638139fa6953811fd008 Mon Sep 17 00:00:00 2001 From: Reginald Braithwaite Date: Wed, 15 May 2013 09:10:32 -0400 Subject: [PATCH 4/5] Remove curry2flipped --- index.js | 1 + package.json | 6 +- test/collections.walk.js | 121 +++++++++++++++++++++++++++++ test/function.arity.js | 10 --- test/index.html | 2 + underscore.collections.walk.js | 99 +++++++++++++++++++++++ underscore.function.combinators.js | 8 +- 7 files changed, 230 insertions(+), 17 deletions(-) create mode 100644 test/collections.walk.js create mode 100644 underscore.collections.walk.js diff --git a/index.js b/index.js index 7972baf..a1c320e 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ ['array.builders', 'array.selectors', + 'collections.walk', 'function.arity', 'function.combinators', 'function.iterators', diff --git a/package.json b/package.json index 1b06477..1020a02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "underscore-contrib", - "version": "0.0.1", + "version": "0.1.0", "main": "index.js", "dependencies": { "underscore": "*" @@ -10,7 +10,7 @@ "url": "https://github.com/documentcloud/underscore-contrib.git" }, "license": "MIT", - "author": {"name": "Fogus", - "email": "me@fogus.me", + "author": {"name": "Fogus", + "email": "me@fogus.me", "url": "http://www.fogus.me"} } diff --git a/test/collections.walk.js b/test/collections.walk.js new file mode 100644 index 0000000..c591cda --- /dev/null +++ b/test/collections.walk.js @@ -0,0 +1,121 @@ +$(document).ready(function() { + + module("underscore.collections.walk"); + + var getSimpleTestTree = function() { + return { + val: 0, + l: { val: 1, l: { val: 2 }, r: { val: 3 } }, + r: { val: 4, l: { val: 5 }, r: { val: 6 } }, + }; + }; + + var getMixedTestTree = function() { + return { + current: + { city: 'Munich', aliases: ['Muenchen'], population: 1378000 }, + previous: [ + { city: 'San Francisco', aliases: ['SF', 'San Fran'], population: 812826 }, + { city: 'Toronto', aliases: ['TO', 'T-dot'], population: 2615000 }, + ] + }; + }; + + test("basic", function() { + // Updates the value of `node` to be the sum of the values of its subtrees. + // Ignores leaf nodes. + var visitor = function(node) { + if (node.l && node.r) + node.val = node.l.val + node.r.val; + }; + + var tree = getSimpleTestTree(); + _.walk.postorder(tree, visitor); + equal(tree.val, 16, 'should visit subtrees first'); + + tree = getSimpleTestTree(); + _.walk.preorder(tree, visitor); + equal(tree.val, 5, 'should visit subtrees after the node itself'); + }); + + test("circularRefs", function() { + var tree = getSimpleTestTree(); + tree.l.l.r = tree; + throws(function() { _.walk.preorder(tree, _.identity) }, TypeError, 'preorder throws an exception'); + throws(function() { _.walk.postrder(tree, _.identity) }, TypeError, 'postorder throws an exception'); + + tree = getSimpleTestTree(); + tree.r.l = tree.r; + throws(function() { _.walk.preorder(tree, _.identity) }, TypeError, 'exception for a self-referencing node'); + }); + + test("simpleMap", function() { + var visitor = function(node, key, parent) { + if (_.has(node, 'val')) return node.val; + if (key !== 'val') throw Error('Leaf node with incorrect key'); + return this.leafChar || '-'; + }; + var visited = _.walk.map(getSimpleTestTree(), _.walk.preorder, visitor).join(''); + equal(visited, '0-1-2-3-4-5-6-', 'pre-order map'); + + visited = _.walk.map(getSimpleTestTree(), _.walk.postorder, visitor).join(''); + equal(visited, '---2-31--5-640', 'post-order map'); + + var context = { leafChar: '*' }; + visited = _.walk.map(getSimpleTestTree(), _.walk.preorder, visitor, context).join(''); + equal(visited, '0*1*2*3*4*5*6*', 'pre-order with context'); + + visited = _.walk.map(getSimpleTestTree(), _.walk.postorder, visitor, context).join(''); + equal(visited, '***2*31**5*640', 'post-order with context'); + + if (document.querySelector) { + var root = document.querySelector('#map-test'); + var ids = _.walk.map(root, _.walk.preorder, function(el) { return el.id; }); + deepEqual(ids, ['map-test', 'id1', 'id2'], 'preorder map with DOM elements'); + + ids = _.walk.map(root, _.walk.postorder, function(el) { return el.id; }); + deepEqual(ids, ['id1', 'id2', 'map-test'], 'postorder map with DOM elements'); + } + }); + + test("mixedMap", function() { + var visitor = function(node, key, parent) { + return _.isString(node) ? node.toLowerCase() : null; + }; + + var tree = getMixedTestTree(); + var preorderResult = _.walk.map(tree, _.walk.preorder, visitor); + equal(preorderResult.length, 19, 'all nodes are visited'); + deepEqual(_.reject(preorderResult, _.isNull), + ['munich', 'muenchen', 'san francisco', 'sf', 'san fran', 'toronto', 'to', 't-dot'], + 'pre-order map on a mixed tree'); + + var postorderResult = _.walk.map(tree, _.walk.postorder, visitor); + deepEqual(preorderResult.sort(), postorderResult.sort(), 'post-order map on a mixed tree'); + + tree = [['foo'], tree]; + var result = _.walk.map(tree, _.walk.postorder, visitor); + deepEqual(_.difference(result, postorderResult), ['foo'], 'map on list of trees'); + }); + + test("pluck", function() { + var tree = getSimpleTestTree(); + tree.val = { val: 'z' }; + + var plucked = _.walk.pluckRec(tree, 'val'); + equal(plucked.shift(), tree.val); + equal(plucked.join(''), 'z123456', 'pluckRec is recursive'); + + plucked = _.walk.pluck(tree, 'val'); + equal(plucked.shift(), tree.val); + equal(plucked.join(''), '123456', 'regular pluck is not recursive'); + + tree.l.r.foo = 42; + equal(_.walk.pluck(tree, 'foo'), 42, 'pluck a value from deep in the tree'); + + tree = getMixedTestTree(); + deepEqual(_.walk.pluck(tree, 'city'), ['Munich', 'San Francisco', 'Toronto'], 'pluck from a mixed tree'); + tree = [tree, { city: 'Loserville', population: 'you' }]; + deepEqual(_.walk.pluck(tree, 'population'), [1378000, 812826, 2615000, 'you'], 'pluck from a list of trees'); + }); +}); diff --git a/test/function.arity.js b/test/function.arity.js index 006a4e1..411d1d3 100644 --- a/test/function.arity.js +++ b/test/function.arity.js @@ -62,14 +62,4 @@ $(document).ready(function() { }); - test("curry2flipped", function () { - - function echo () { return [].slice.call(arguments, 0); } - - deepEqual(_.flip2(echo)(1, 2), [2, 1], "Control test"); - deepEqual(_.curry2flipped(echo)(1, 2), [2, 1], "Accepts arguments greedily"); - deepEqual(_.curry2flipped(echo)(1)(2), [2, 1], "Accepts curried arguments"); - - }); - }); diff --git a/test/index.html b/test/index.html index e905c3b..b0c8b9e 100644 --- a/test/index.html +++ b/test/index.html @@ -11,6 +11,7 @@ + @@ -23,6 +24,7 @@ + diff --git a/underscore.collections.walk.js b/underscore.collections.walk.js new file mode 100644 index 0000000..04a3fd3 --- /dev/null +++ b/underscore.collections.walk.js @@ -0,0 +1,99 @@ +// Underscore-contrib (underscore.collections.walk.js 0.0.1) +// (c) 2013 Patrick Dubroy +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + // An internal object that can be returned from a visitor function to + // prevent a top-down walk from walking subtrees of a node. + var breaker = {}; + + var notTreeError = 'Not a tree: same object found in two different branches'; + + // Walk the tree recursively beginning with `root`, calling `beforeFunc` + // before visiting an objects descendents, and `afterFunc` afterwards. + function walk(root, beforeFunc, afterFunc, context) { + var visited = []; + (function _walk(value, key, parent) { + if (beforeFunc && beforeFunc.call(context, value, key, parent) === breaker) + return; + + if (_.isObject(value) || _.isArray(value)) { + // Keep track of objects that have been visited, and throw an exception + // when trying to visit the same object twice. + if (visited.indexOf(value) >= 0) throw new TypeError(notTreeError); + visited.push(value); + + // Recursively walk this object's descendents. If it's a DOM node, walk + // its DOM children. + _.each(_.isElement(value) ? value.children : value, _walk, context); + } + + if (afterFunc) afterFunc.call(context, value, key, parent); + })(root); + } + + function pluck(obj, propertyName, recursive) { + var results = []; + _.walk.preorder(obj, function(value, key) { + if (key === propertyName) { + results[results.length] = value; + if (!recursive) return breaker; + } + }); + return results; + } + + // Add the `walk` namespace + // ------------------------ + + _.walk = walk; + _.extend(walk, { + // Recursively traverses `obj` in a depth-first fashion, invoking the + // `visitor` function for each object only after traversing its children. + postorder: function(obj, visitor, context) { + walk(obj, null, visitor, context); + }, + + // Recursively traverses `obj` in a depth-first fashion, invoking the + // `visitor` function for each object before traversing its children. + preorder: function(obj, visitor, context) { + walk(obj, visitor, null, context) + }, + + // Produces a new array of values by recursively traversing `obj` and + // mapping each value through the transformation function `visitor`. + // `strategy` is the traversal function to use, e.g. `preorder` or + // `postorder`. + map: function(obj, strategy, visitor, context) { + var results = []; + strategy.call(null, obj, function(value, key, parent) { + results[results.length] = visitor.call(context, value, key, parent); + }); + return results; + }, + + // Return the value of properties named `propertyName` reachable from the + // tree rooted at `obj`. Results are not recursively searched; use + // `pluckRec` for that. + pluck: function(obj, propertyName) { + return pluck(obj, propertyName, false); + }, + + // Version of `pluck` which recursively searches results for nested objects + // with a property named `propertyName`. + pluckRec: function(obj, propertyName) { + return pluck(obj, propertyName, true); + } + }); + _.walk.collect = _.walk.map; // Alias `map` as `collect`. +})(this); diff --git a/underscore.function.combinators.js b/underscore.function.combinators.js index 0e33289..7fa5007 100644 --- a/underscore.function.combinators.js +++ b/underscore.function.combinators.js @@ -163,10 +163,6 @@ // map the arguments of a function mapArgs: _.curry2(baseMapArgs), - - // map the arguments of a function, takes the mapping function - // first so it can be used as a combinator - mapArgsWith: _.curry2flipped(baseMapArgs), // Returns a function that returns an array of the calls to each // given function for some arguments. @@ -226,6 +222,10 @@ }); _.unsplatr = _.unsplat; + + // map the arguments of a function, takes the mapping function + // first so it can be used as a combinator + _.mapArgsWith = _.curry2(_.flip(baseMapArgs)); From 6ff338f65c26d0426b92be61b07334c42583dd28 Mon Sep 17 00:00:00 2001 From: Reginald Braithwaite Date: Wed, 15 May 2013 09:16:28 -0400 Subject: [PATCH 5/5] A few more tests to confirm that the rewritten mapArgsWith has the proper behaviour. Yup! --- test/function.combinators.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/function.combinators.js b/test/function.combinators.js index b777efa..93b0123 100644 --- a/test/function.combinators.js +++ b/test/function.combinators.js @@ -99,6 +99,10 @@ $(document).ready(function() { deepEqual(_.mapArgsWith(double, echo)(), [], "should handle the empty case") deepEqual(_.mapArgsWith(double, echo)(42), [84], "should handle one arg") deepEqual(_.mapArgsWith(plusOne, echo)(1, 2, 3), [2, 3, 4], "should handle many args") + + deepEqual(_.mapArgsWith(double)(echo)(), [], "should handle the empty case") + deepEqual(_.mapArgsWith(double)(echo)(42), [84], "should handle one arg") + deepEqual(_.mapArgsWith(plusOne)(echo)(1, 2, 3), [2, 3, 4], "should handle many args") }); test("flip2", function() {