diff --git a/build.js b/build.js index aa4e6a3fc..99c68240c 100644 --- a/build.js +++ b/build.js @@ -120,6 +120,7 @@ 'contains': ['baseEach', 'getIndexOf', 'isString'], 'countBy': ['createAggregator'], 'createCallback': ['baseCreateCallback', 'baseIsEqual', 'isObject', 'keys'], + 'curry': ['createBound'], 'debounce': ['isObject'], 'defaults': ['createIterator'], 'defer': ['bind'], @@ -390,6 +391,7 @@ 'bindKey', 'createCallback', 'compose', + 'curry', 'debounce', 'defer', 'delay', @@ -560,6 +562,7 @@ 'bindKey', 'cloneDeep', 'createCallback', + 'curry', 'findIndex', 'findKey', 'findLast', @@ -3867,7 +3870,7 @@ if (!isLodash('range')) { source = source.replace(matchFunction(source, 'range'), function(match) { return match - .replace(/typeof *step[^:]+:/, '+step ||') + .replace(/typeof *step[^:]+:/, '') .replace(/\(step.*\|\|.+?\)/, 'step') }); } diff --git a/build/pre-compile.js b/build/pre-compile.js index 42ed6eed6..ad34b0f27 100644 --- a/build/pre-compile.js +++ b/build/pre-compile.js @@ -111,6 +111,7 @@ 'countBy', 'createCallback', 'criteria', + 'curry', 'debounce', 'defaults', 'defer', diff --git a/lodash.js b/lodash.js index bb0e9c186..cefc1170e 100644 --- a/lodash.js +++ b/lodash.js @@ -1059,7 +1059,7 @@ setBindData(func, bindData); } // exit early if there are no `this` references or `func` is bound - if (bindData !== true && !(bindData && bindData[4])) { + if (bindData !== true && !(bindData && bindData[1] & 1)) { return func; } switch (argCount) { @@ -1416,44 +1416,57 @@ } /** - * Creates a function that, when called, invokes `func` with the `this` binding - * of `thisArg` and prepends any `partialArgs` to the arguments provided to the - * bound function. + * Creates a function that, when called, either curries or invokes `func` + * with an optional `this` binding and partially applied arguments. * * @private - * @param {Function|String} func The function to bind or the method name. - * @param {Mixed} thisArg The `this` binding of `func`. - * @param {Array} partialArgs An array of arguments to be prepended to those provided to the new function. - * @param {Array} partialRightArgs An array of arguments to be appended to those provided to the new function. - * @param {Boolean} [isPartial=false] A flag to indicate performing only partial application. - * @param {Boolean} [isAlt=false] A flag to indicate `_.bindKey` or `_.partialRight` behavior. + * @param {Function|String} func The function or method name to reference. + * @param {Number} bitmask The bitmask of method flags to compose. + * The bitmask may be composed of the following flags: + * 1 - `_.bind` + * 2 - `_.bindKey` + * 4 - `_.curry` + * 8 - `_.partial` + * 16 - `_.partialRight` + * @param {Array} [partialArgs] An array of arguments to prepend to those + * provided to the new function. + * @param {Array} [partialRightArgs] An array of arguments to append to those + * provided to the new function. + * @param {Mixed} [thisArg] The `this` binding of `func`. + * @param {Number} [arity] The arity of `func`. * @returns {Function} Returns the new bound function. */ - function createBound(func, thisArg, partialArgs, partialRightArgs, isPartial, isAlt) { - var isBindKey = isAlt && !isPartial, - isFunc = isFunction(func); + function createBound(func, bitmask, partialArgs, partialRightArgs, thisArg, arity) { + var isBind = bitmask & 1, + isBindKey = bitmask & 2, + isCurry = bitmask & 4, + isPartialRight = bitmask & 16; - // throw if `func` is not a function when not behaving as `_.bindKey` - if (!isFunc && !isBindKey) { + if (!isBindKey && !isFunction(func)) { throw new TypeError; } - var args = func.__bindData__; - if (args) { - push.apply(args[2], partialArgs); - push.apply(args[3], partialRightArgs); - - // add `thisArg` to previous `_.partial` and `_.partialRight` arguments - if (!isPartial && args[4]) { - args[1] = thisArg; - args[4] = false; - args[5] = isAlt; + var bindData = func && func.__bindData__; + if (bindData) { + if (isBind && !(bindData[1] & 1)) { + bindData[4] = thisArg; } - return createBound.apply(null, args); + if (isCurry && !(bindData[1] & 4)) { + bindData[5] = arity; + } + if (partialArgs) { + push.apply(bindData[2] || (bindData[2] = []), partialArgs); + } + if (partialRightArgs) { + push.apply(bindData[3] || (bindData[3] = []), partialRightArgs); + } + bindData[1] |= bitmask; + return createBound.apply(null, bindData); } // use `Function#bind` if it exists and is fast // (in V8 `Function#bind` is slower except when partially applied) - if (!isPartial && !isAlt && !partialRightArgs.length && (support.fastBind || (nativeBind && partialArgs.length))) { - args = [func, thisArg]; + if (isBind && !(isBindKey || isCurry || isPartialRight) && + (support.fastBind || (nativeBind && partialArgs.length))) { + var args = [func, thisArg]; push.apply(args, partialArgs); var bound = nativeBind.call.apply(nativeBind, args); } @@ -1462,15 +1475,22 @@ // `Function#bind` spec // http://es5.github.io/#x15.3.4.5 var args = arguments, - thisBinding = isPartial ? this : thisArg; + thisBinding = isBind ? thisArg : this; - if (isBindKey) { - func = thisArg[key]; - } - if (partialArgs.length || partialRightArgs.length) { + if (partialArgs) { unshift.apply(args, partialArgs); + } + if (partialRightArgs) { push.apply(args, partialRightArgs); } + if (isCurry && args.length < arity) { + bindData[2] = args; + bindData[3] = null; + return createBound(bound, bitmask & ~8 & ~16); + } + if (isBindKey) { + func = thisBinding[key]; + } if (this instanceof bound) { // ensure `new bound` is an instance of `func` thisBinding = createObject(func.prototype); @@ -1484,12 +1504,12 @@ }; } // take a snapshot of `arguments` before juggling - args = nativeSlice.call(arguments); + bindData = nativeSlice.call(arguments); if (isBindKey) { var key = thisArg; thisArg = func; } - setBindData(bound, args); + setBindData(bound, bindData); return bound; } @@ -4628,7 +4648,7 @@ */ function range(start, end, step) { start = +start || 0; - step = typeof step == 'number' ? step : 1; + step = typeof step == 'number' ? step : (+step || 1); if (end == null) { end = start; @@ -4678,6 +4698,8 @@ * * console.log(evens); * // => [2, 4, 6] + * + * */ function remove(array, callback, thisArg) { var index = -1, @@ -5043,7 +5065,7 @@ * // => 'hi moe' */ function bind(func, thisArg) { - return createBound(func, thisArg, nativeSlice.call(arguments, 2), []); + return createBound(func, 9, nativeSlice.call(arguments, 2), null, thisArg); } /** @@ -5117,7 +5139,7 @@ * // => 'hi, moe!' */ function bindKey(object, key) { - return createBound(object, key, nativeSlice.call(arguments, 2), [], false, true); + return createBound(object, 11, nativeSlice.call(arguments, 2), null, key); } /** @@ -5231,6 +5253,39 @@ }; } + /** + * Creates a function which accepts one or more arguments of `func` that when + * invoked either executes `func` returning its result, if all `func` arguments + * have been provided, or returns a function that accepts one or more of the + * remaining `func` arguments, and so on. The arity of `func` can be specified + * if `func.length` is not sufficient. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to curry. + * @param {Number} [arity=func.length] The arity of `func`. + * @returns {Function} Returns the new curried function. + * @example + * + * var curried = _.curry(function(a, b, c) { + * console.log(a + b + c); + * }); + * + * curried(1)(2)(3); + * // => 6 + * + * curried(1, 2)(3); + * // => 6 + * + * curried(1, 2, 3); + * // => 6 + */ + function curry(func, arity) { + arity = typeof arity == 'number' ? arity : (+arity || func.length); + return createBound(func, 4, null, null, null, arity); + } + /** * Creates a function that will delay the execution of `func` until after * `wait` milliseconds have elapsed since the last time it was invoked. @@ -5484,7 +5539,7 @@ * // => 'hi moe' */ function partial(func) { - return createBound(func, null, nativeSlice.call(arguments, 1), [], true); + return createBound(func, 8, nativeSlice.call(arguments, 1)); } /** @@ -5515,7 +5570,7 @@ * // => { '_': _, 'jq': $ } */ function partialRight(func) { - return createBound(func, null, [], nativeSlice.call(arguments, 1), true, true); + return createBound(func, 16, null, nativeSlice.call(arguments, 1)); } /** @@ -6181,6 +6236,7 @@ lodash.compose = compose; lodash.countBy = countBy; lodash.createCallback = createCallback; + lodash.curry = curry; lodash.debounce = debounce; lodash.defaults = defaults; lodash.defer = defer; diff --git a/test/test-build.js b/test/test-build.js index 3f9b0d5af..a98796b09 100644 --- a/test/test-build.js +++ b/test/test-build.js @@ -159,6 +159,7 @@ 'bindKey', 'createCallback', 'compose', + 'curry', 'debounce', 'defer', 'delay', @@ -289,6 +290,7 @@ 'bindKey', 'cloneDeep', 'createCallback', + 'curry', 'findIndex', 'findKey', 'findLast', diff --git a/test/test.js b/test/test.js index 454ef7ab1..810e65c20 100644 --- a/test/test.js +++ b/test/test.js @@ -664,6 +664,38 @@ /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.curry'); + + (function() { + test('should curry based on the number of arguments provided', function() { + function func(a, b, c) { + return a + b + c; + } + + var curried = _.curry(func); + + equal(curried(1)(2)(3), 6); + equal(curried(1, 2)(3), 6); + equal(curried(1, 2, 3), 6); + }); + + test('should not alter the `this` binding', function() { + function func(a, b, c) { + return this[a] + this[b] + this[c]; + } + + var object = { 'a': 1, 'b': 2, 'c': 3 }; + + equal(_.curry(_.bind(func, object), 3)('a')('b')('c'), 6); + equal(_.bind(_.curry(func), object)('a')('b')('c'), 6); + + object.func = _.curry(func); + equal(object.func('a', 'b', 'c'), 6); + }); + }()); + + /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.debounce'); (function() { @@ -1700,7 +1732,7 @@ deepEqual(_.initial(array, 0), [1, 2, 3]); }); - test('should allow a falsey `array` argument', function() { + test('should accept a falsey `array` argument', function() { _.forEach(falsey, function(index, value) { try { var actual = index ? _.initial(value) : _.initial(); @@ -2585,12 +2617,15 @@ equal(func(fn)(arg), arg); }); - test('`_.' + methodName + '` should not alter the `this` binding of either function', function() { + test('`_.' + methodName + '` should not alter the `this` binding', function() { var object = { 'a': 1 }, fn = function() { return this.a; }; strictEqual(func(_.bind(fn, object))(), object.a); strictEqual(_.bind(func(fn), object)(), object.a); + + object.fn = func(fn); + strictEqual(object.fn(), object.a); }); }); @@ -2974,7 +3009,7 @@ deepEqual(_.rest(array, 0), [1, 2, 3]); }); - test('should allow a falsey `array` argument', function() { + test('should accept a falsey `array` argument', function() { _.forEach(falsey, function(index, value) { try { var actual = index ? _.rest(value) : _.rest(); @@ -3066,7 +3101,7 @@ (function() { var args = arguments; - test('should allow a falsey `object` argument', function() { + test('should accept a falsey `object` argument', function() { _.forEach(falsey, function(index, value) { try { var actual = index ? _.size(value) : _.size(); @@ -4074,9 +4109,9 @@ deepEqual(_.values(args), [[3]], message('remove')); }); - test('should allow falsey primary arguments', function() { + test('should accept falsey primary arguments', function() { function message(methodName) { - return '`_.' + methodName + '` should allow falsey primary arguments'; + return '`_.' + methodName + '` should accept falsey primary arguments'; } deepEqual(_.difference(null, array), [], message('difference')); @@ -4084,9 +4119,9 @@ deepEqual(_.union(null, array), array, message('union')); }); - test('should allow falsey secondary arguments', function() { + test('should accept falsey secondary arguments', function() { function message(methodName) { - return '`_.' + methodName + '` should allow falsey secondary arguments'; + return '`_.' + methodName + '` should accept falsey secondary arguments'; } deepEqual(_.difference(array, null), array, message('difference')); @@ -4100,63 +4135,64 @@ QUnit.module('lodash methods'); (function() { - test('should allow falsey arguments', function() { + var allMethods = _.reject(_.functions(_), function(methodName) { + return /^_/.test(methodName); + }); + + var returnArrays = [ + 'at', + 'compact', + 'difference', + 'filter', + 'flatten', + 'functions', + 'initial', + 'intersection', + 'invoke', + 'keys', + 'map', + 'pairs', + 'pluck', + 'range', + 'reject', + 'rest', + 'shuffle', + 'sortBy', + 'times', + 'toArray', + 'union', + 'uniq', + 'values', + 'where', + 'without', + 'zip' + ]; + + var rejectFalsey = [ + 'after', + 'bind', + 'compose', + 'curry', + 'debounce', + 'defer', + 'delay', + 'memoize', + 'once', + 'partial', + 'partialRight', + 'tap', + 'throttle', + 'wrap' + ]; + + var acceptFalsey = _.difference(allMethods, rejectFalsey); + + test('should accept falsey arguments', function() { var isExported = '_' in window, oldDash = window._; - var returnArrays = [ - 'at', - 'compact', - 'difference', - 'filter', - 'flatten', - 'functions', - 'initial', - 'intersection', - 'invoke', - 'keys', - 'map', - 'pairs', - 'pluck', - 'range', - 'reject', - 'rest', - 'shuffle', - 'sortBy', - 'times', - 'toArray', - 'union', - 'uniq', - 'values', - 'where', - 'without', - 'zip' - ]; - var allMethods = _.reject(_.functions(_), function(methodName) { - return /^_/.test(methodName); - }); - - var funcs = _.difference(allMethods, [ - 'after', - 'bind', - 'bindAll', - 'bindKey', - 'compose', - 'debounce', - 'defer', - 'delay', - 'functions', - 'memoize', - 'once', - 'partial', - 'partialRight', - 'tap', - 'throttle', - 'wrap' - ]); - - _.forEach(funcs, function(methodName) { + _.forEach(acceptFalsey, function(methodName) { var actual = [], expected = _.map(falsey, function() { return []; }), func = _[methodName], @@ -4180,7 +4216,27 @@ if (_.indexOf(returnArrays, methodName) > -1) { deepEqual(actual, expected, '_.' + methodName + ' returns an array'); } - ok(pass, '`_.' + methodName + '` allows falsey arguments'); + ok(pass, '`_.' + methodName + '` accepts falsey arguments'); + }); + }); + + test('should reject falsey arguments', function() { + _.forEach(rejectFalsey, function(methodName) { + var actual = [], + expected = _.map(falsey, function() { return true; }), + func = _[methodName]; + + _.forEach(falsey, function(value, index) { + var pass = false; + try { + index ? func(value) : func(); + } catch(e) { + pass = true; + } + actual.push(pass); + }); + + deepEqual(actual, expected, '`_.' + methodName + '` rejects falsey arguments'); }); });