From 52d9e45378b81bc46144ee363f8794642f834b31 Mon Sep 17 00:00:00 2001 From: Marcus Stade Date: Fri, 18 Apr 2014 15:10:48 +0200 Subject: [PATCH 1/5] Improved `variadic` tests and change behavior of `rest` Previously, `rest` was always defined and passed, even if the original call didn't supply enough arguments to reach `rest`. This poses some problems when you expect the length of `arguments` to match that of the original call to the variadic function, so now `rest` is only supplied if there are actual arguments to fill it up with. This means it'll be undefined otherwise, and `arguments` will be identical to the original call. Additionally, I changed `variadic` so that it'll correctly report `length` for the wrapped function, provided the wrapped function `length` is 26 or below. I'd be hard pressed to remember ever seeing a function with more than even 10 parameters, but still. This isn't an ideal solution of course, but probably hits home 99.99% of the time which is enough. --- lib/variadic.js | 58 +++++++++++++++++------- test/variadic.js | 114 +++++++++++++++++++++++++++++++---------------- 2 files changed, 117 insertions(+), 55 deletions(-) diff --git a/lib/variadic.js b/lib/variadic.js index f2d2e27..4634a17 100644 --- a/lib/variadic.js +++ b/lib/variadic.js @@ -2,23 +2,49 @@ module.exports = variadic function variadic(fn) { if (isnt(Function, fn)) throw new TypeError('Parameter `fn` must be a function.') + + const argc = fn.length - 1 - const argc = fn.length - 1 - - if (argc) { - return function variadic() { - const head = slice(arguments, 0, argc) - , tail = slice(arguments, argc) - - return fn.apply(this, head.concat([tail])) - } - } else { - return function variadic() { - const rest = slice(arguments) - return fn.call(this, rest) - } + switch (argc) { + case -1 : return fn + case 0 : return function (rest) { return apply(fn, this, argc, slice(arguments)) } + case 1 : return function (a, rest) { return apply(fn, this, argc, slice(arguments)) } + case 2 : return function (a, b, rest) { return apply(fn, this, argc, slice(arguments)) } + case 3 : return function (a, b, c, rest) { return apply(fn, this, argc, slice(arguments)) } + case 4 : return function (a, b, c, d, rest) { return apply(fn, this, argc, slice(arguments)) } + case 5 : return function (a, b, c, d, e, rest) { return apply(fn, this, argc, slice(arguments)) } + case 6 : return function (a, b, c, d, e, f, rest) { return apply(fn, this, argc, slice(arguments)) } + case 7 : return function (a, b, c, d, e, f, g, rest) { return apply(fn, this, argc, slice(arguments)) } + case 8 : return function (a, b, c, d, e, f, g, h, rest) { return apply(fn, this, argc, slice(arguments)) } + case 9 : return function (a, b, c, d, e, f, g, h, i, rest) { return apply(fn, this, argc, slice(arguments)) } + case 10 : return function (a, b, c, d, e, f, g, h, i, j, rest) { return apply(fn, this, argc, slice(arguments)) } + case 11 : return function (a, b, c, d, e, f, g, h, i, j, k, rest) { return apply(fn, this, argc, slice(arguments)) } + case 12 : return function (a, b, c, d, e, f, g, h, i, j, k, l, rest) { return apply(fn, this, argc, slice(arguments)) } + case 13 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, rest) { return apply(fn, this, argc, slice(arguments)) } + case 14 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, rest) { return apply(fn, this, argc, slice(arguments)) } + case 15 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, rest) { return apply(fn, this, argc, slice(arguments)) } + case 16 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, rest) { return apply(fn, this, argc, slice(arguments)) } + case 17 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, rest) { return apply(fn, this, argc, slice(arguments)) } + case 18 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, rest) { return apply(fn, this, argc, slice(arguments)) } + case 19 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, rest) { return apply(fn, this, argc, slice(arguments)) } + case 20 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, rest) { return apply(fn, this, argc, slice(arguments)) } + case 21 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, rest) { return apply(fn, this, argc, slice(arguments)) } + case 22 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, rest) { return apply(fn, this, argc, slice(arguments)) } + case 23 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, rest) { return apply(fn, this, argc, slice(arguments)) } + case 24 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, rest) { return apply(fn, this, argc, slice(arguments)) } + case 25 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, rest) { return apply(fn, this, argc, slice(arguments)) } + default : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, rest) { return apply(fn, this, argc, slice(arguments)) } } } -const slice = require('./slice') - , isnt = require('./isnt') \ No newline at end of file +function apply(fn, host, argc, argv) { + const rest = slice(argv, argc) + argv = slice(argv, 0, argc) + return isEmpty(rest)? fn.apply(host, argv) : fn.apply(host, argv.concat([rest])) +} + +const isEmpty = require('./isEmpty') + , slice = require('./slice') + , isnt = require('./isnt') + , min = require('./min') + , max = Math.max \ No newline at end of file diff --git a/test/variadic.js b/test/variadic.js index 5fe7def..9db8d6f 100644 --- a/test/variadic.js +++ b/test/variadic.js @@ -1,60 +1,96 @@ const variadic = require('../lib/variadic') , expect = require('chai').expect + , thunk = require('../lib/thunk') + , range = require('../lib/range') , slice = require('../lib/slice') , each = require('../lib/each') , src = require('../lib/src') -describe('variadic', function() { - describe('when called with a non-function parameter', function() { - it('should throw a TypeError', function() { - each( - [ 1, 0 - , '', 'hello' - , true, false - , null, undefined - ] - , function(test) { - expect(function() { variadic(test) }).to.throw(TypeError) - } - ) +describe('`variadic`', function() { + describe('given an argument `fn`', function() { + describe('when it is not a function', function() { + it('should throw a `TypeError`', function() { + each( + [ 1, 0 + , '', 'hello' + , true, false + , null, undefined + ] + , function(fn) { + + expect(function() { variadic(fn) }).to.throw(TypeError) + } + ) + }) }) - }) - var verify + describe('when it is a function', function() { + describe('of arity 0', function() { + it('should just return `fn`', function() { + const fn = function() {} + expect(variadic(fn)).to.equal(fn) + }) + }) - each( - [ [function(rest) { verify(arguments) }, [1, 2, 3], ['foo', false], [function() {}], []] - , [function foo(a, b, rest) { verify(arguments) }, [1, 2, 'hello', 'world!'], ['foo', 'bar', 'baz'], [1, 2]] - ] - , - function(test) { - const fn = test[0] + each(range(1, 26), function(n) { + describe('of arity '+n, function() { + const sig = slice('abcdefghijklmnopqrstuvwxyz', 0, n - 1).concat('rest') - describe('when given `' + src(fn) + '`', function() { - const varfn = variadic(fn) + it('should return a '+n+' arity function', function() { + const fn = Function.apply(Function, sig.concat('return')) - each(test.slice(1), function(args) { - const argsrc = args.length? '`' + src(args).slice(1, -1) + '`' : 'nothing' + expect(fn).to.have.length(n) + expect(variadic(fn)).to.have.length(n) + }) - describe('and when called with ' + argsrc, function() { - const expHead = slice(args, 0, fn.length - 1) - , expRest = slice(args, fn.length - 1) + describe('that when called with fewer than '+n+' arguments', function() { + it('should leave `rest` undefined', function() { + var args = slice(sig, 0, -1) - it('should set rest to `' + src(expRest) + '`', function(done) { - verify = function(actual) { - const head = slice(actual, 0, -1) - , rest = actual[actual.length - 1] + while (args.length) { + var fn = Function.apply(Function, sig.concat( + [ 'this.expect(arguments).to.have.length('+args.length+')' + , 'this.expect(rest).to.be.undefined' + ].join('\n') + )) - expect(head).to.eql(expHead) - expect(rest).to.eql(expRest) - done() + variadic(fn).apply({ expect: expect }, args) + args = slice(args, 0, -1) } + }) + }) + + describe('but when called with '+n+' arguments', function() { + it('should put the last argument in `rest`', function() { + const args = slice(sig, 0, -1).concat('wibble') - varfn.apply(null, args) + const fn = Function.apply(Function, sig.concat( + [ 'this.expect(arguments).to.have.length('+n+')' + , 'this.expect(rest).to.have.length(1)' + , 'this.expect(rest).to.contain("wibble")' + ].join('\n') + )) + + variadic(fn).apply({ expect: expect }, args) + }) + }) + + describe('and when called with more than '+n+' arguments', function() { + it('should put all variadic arguments in `rest`', function() { + const args = slice(sig, 0, -1).concat('wibble', 'wobble', 'bob') + + const fn = Function.apply(Function, sig.concat( + [ 'this.expect(arguments).to.have.length('+n+')' + , 'this.expect(rest).to.have.length(3)' + , 'this.expect(rest).to.eql(["wibble", "wobble", "bob"])' + ].join('\n') + )) + + variadic(fn).apply({ expect: expect }, args) }) }) }) }) - } - ) + }) + }) }) \ No newline at end of file From 07498ce5cf931bc9fcdc284affc4d0b245d58671 Mon Sep 17 00:00:00 2001 From: Marcus Stade Date: Fri, 2 May 2014 01:06:54 +0200 Subject: [PATCH 2/5] Added `meta` for adding metadata to objects Eventually, there will be some standard metadata, possibly different for different types of objects. Likely, the only distinction will be made for functions, which may have useful metadata such as composition information or other things which could be useful when debugging. --- lib/meta.js | 28 +++++++++++++++++ test/meta.js | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 lib/meta.js create mode 100644 test/meta.js diff --git a/lib/meta.js b/lib/meta.js new file mode 100644 index 0000000..beae0c9 --- /dev/null +++ b/lib/meta.js @@ -0,0 +1,28 @@ +module.exports = meta + +const protoOf = Object.getPrototypeOf + , mutable = Object.isExtensible + , define = Object.defineProperty + , create = Object.create + , names = Object.getOwnPropertyNames + , isnt = require("./isnt") + , has = require("./has") + +function meta(x, data) { + if (isnt(x)) return null + + if (arguments.length < 2) return (x[meta.key] || null) + + if (!mutable(x)) throw TypeError("Can't add metadata to non-extensible object.") + + const metadata = x[meta.key] || define(x, meta.key, { value: create(null) })[meta.key] + + names(data).forEach(function(name) { + if (has(metadata, name)) throw TypeError("Can't change immutable metadata: "+name) + define(metadata, name, { value: data[name], enumerable: true }) + }) + + return metadata +} + +define(meta, "key", { value: "^metadata:{}" }) \ No newline at end of file diff --git a/test/meta.js b/test/meta.js new file mode 100644 index 0000000..71d3c56 --- /dev/null +++ b/test/meta.js @@ -0,0 +1,85 @@ +const expect = require("chai").expect + , each = require("../lib/each") + , meta = require("../lib/meta") + , $ = require("../lib/partial") + +describe("`meta`", function() { + describe("given an object `x`", function() { + describe("when `x` is logically false", function() { + it("should return `null`", function() { + each([null, false, undefined], function(x) { + expect(meta(x)).to.be.null + }) + }) + }) + + describe("when called with no other parameters", function() { + describe("and when `x` is associated with metadata", function() { + it("should return the metadata object", function() { + const m = { foo: "wibble" } + , x = {} + + meta(x, m) + expect(meta(x)).to.eql(m) + }) + }) + + describe("but when `x` is not associated with metadata", function() { + it("should return null", function() { + expect(meta({})).to.be.null + }) + }) + }) + + describe("and also given a map `data`", function() { + describe("when `x` is not extensible", function() { + it("should throw an error", function() { + each( + [ Object.preventExtensions({}) + , Object.freeze({}) + , Object.seal({}) + ] + , function(x) { + expect($(meta, x, {})).to.throw() + } + ) + }) + }) + + describe("when `x` is not associated with any metadata", function() { + it("should set all values of `data` and return the full metadata object", function() { + const m = { foo: "wibble" } + , x = {} + + expect(meta(x, m)).to.eql(m) + expect(meta(x)).to.eql(m) + }) + }) + + describe("when `x` is associated with metadata", function() { + describe("and when no properties of `data` overlap", function() { + it("should set all values of `data` and return the full metadata object", function() { + const a = { foo: "wibble" } + , b = { bob: "wobble" } + , c = { foo: "wibble", bob: "wobble" } + , x = {} + + expect(meta(x, a)).to.eql(a) + expect(meta(x, b)).to.eql(c) + }) + }) + + describe("but when any one property of `data` overlap", function() { + it("should throw an error", function() { + const a = { foo: "wibble" } + , b = { foo: "wobble" } + , x = {} + + expect(meta(x, a)).to.eql(a) + expect($(meta, x, b)).to.throw() + }) + }) + }) + }) + }) +}) \ No newline at end of file From 8103da3959864e33a69fd4c0c7af63c7fbebe3c6 Mon Sep 17 00:00:00 2001 From: Marcus Stade Date: Thu, 24 Apr 2014 23:43:26 +0200 Subject: [PATCH 3/5] Fixed some errors and commit mishaps --- lib/has.js | 8 ++++++++ lib/thunk.js | 14 ++++---------- test/thunk.js | 41 ++++++++++++----------------------------- 3 files changed, 24 insertions(+), 39 deletions(-) create mode 100644 lib/has.js diff --git a/lib/has.js b/lib/has.js new file mode 100644 index 0000000..89d2c38 --- /dev/null +++ b/lib/has.js @@ -0,0 +1,8 @@ +module.exports = has + +function has(x, key) { + return is(x) && hasprop.call(x, key) +} + +const hasprop = Object.prototype.hasOwnProperty + , is = require("./is") \ No newline at end of file diff --git a/lib/thunk.js b/lib/thunk.js index 4d253ff..f64c530 100644 --- a/lib/thunk.js +++ b/lib/thunk.js @@ -1,19 +1,12 @@ module.exports = require('./variadic')(thunk) function thunk(fn, rest) { - if (isnt(Function, fn)) { - rest = [fn] - fn = identity - } + if (isnt(Function, fn)) throw new TypeError('Expected `fn` to be a function.') - if (rest.length > 1) { + if (is(rest)) { return function thunk() { return fn.apply(this, rest) } - } else if (rest.length) { - return function thunk() { - return fn.call(this, rest[0]) - } } else { return function thunk() { return fn.call(this) @@ -23,4 +16,5 @@ function thunk(fn, rest) { const identity = require('./identity') , slice = require('./slice') - , isnt = require('./isnt') \ No newline at end of file + , isnt = require('./isnt') + , is = require('./is') \ No newline at end of file diff --git a/test/thunk.js b/test/thunk.js index 0d0e47f..7ae9c3f 100644 --- a/test/thunk.js +++ b/test/thunk.js @@ -214,35 +214,18 @@ describe('thunk', function() { }) describe('when given a non-function `val` as the first parameter', function() { - it('should return a zero arity thunk', function() { - const th = thunk(function() {}) - - expect(th).to.be.a('function') - expect(th.length).to.equal(0) - expect(th.name).to.equal('thunk') - }) - - describe('and when called', function() { - it('should return `val` and nothing else', function() { - each( - [ 0 - , 1 - , true - , false - , '' - , 'hello' - , [] - , {} - , null - , undefined - ] - , - function(val) { - const th = thunk(val) - expect(th()).to.equal(val) - } - ) - }) + it('should throw a TypeError', function() { + each( + [ 0, 1 + , true, false + , '', 'hello' + , [], {} + , null, undefined + ] + , function(val) { + expect(thunk(thunk, val)).to.throw(TypeError) + } + ) }) }) }) \ No newline at end of file From 13a800a2edf5013092345e901ee9bbf835ffbebe Mon Sep 17 00:00:00 2001 From: Marcus Stade Date: Fri, 2 May 2014 01:16:54 +0200 Subject: [PATCH 4/5] Made sure variadic test covers all cases --- test/variadic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/variadic.js b/test/variadic.js index 9db8d6f..7d4cb69 100644 --- a/test/variadic.js +++ b/test/variadic.js @@ -32,7 +32,7 @@ describe('`variadic`', function() { }) }) - each(range(1, 26), function(n) { + each(range(1, 28), function(n) { describe('of arity '+n, function() { const sig = slice('abcdefghijklmnopqrstuvwxyz', 0, n - 1).concat('rest') From 1d415a6711d75684461ca631006a1fc31eea3b43 Mon Sep 17 00:00:00 2001 From: Marcus Stade Date: Fri, 2 May 2014 01:29:43 +0200 Subject: [PATCH 5/5] Remove unused `isnt` import from `not` --- lib/not.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/not.js b/lib/not.js index 0f5b812..1684f24 100644 --- a/lib/not.js +++ b/lib/not.js @@ -2,6 +2,4 @@ module.exports = not function not(x) { return x === false || x == null -} - -const isnt = require('./isnt') \ No newline at end of file +} \ No newline at end of file