Repository: gcanti/tcomb
Pull request #201 (2016-05-05 15:20:14)

func combinator: support optional arguments. Ref #198


Patch:
diff --git a/lib/func.js b/lib/func.js
index 17e8810..67dc105 100644
--- a/lib/func.js
+++ b/lib/func.js
@@ -10,6 +10,7 @@ var isBoolean = require('./isBoolean');
 var tuple = require('./tuple');
 var getFunctionName = require('./getFunctionName');
 var getTypeName = require('./getTypeName');
+var isType = require('./isType');
 
 function getDefaultName(domain, codomain) {
   return '(' + domain.map(getTypeName).join(', ') + ') => ' + getTypeName(codomain);
@@ -19,6 +20,20 @@ function isInstrumented(f) {
   return FunctionType.is(f) && isObject(f.instrumentation);
 }
 
+function getOptionalArgumentsIndex(types) {
+  var end = types.length;
+  var areAllMaybes = false;
+  for (var i = end - 1; i >= 0; i--) {
+    var type = types[i];
+    if (!isType(type) || type.meta.kind !== 'maybe') {
+      return (i + 1);
+    } else {
+      areAllMaybes = true;
+    }
+  }
+  return areAllMaybes ? 0 : end;
+}
+
 function func(domain, codomain, name) {
 
   domain = isArray(domain) ? domain : [domain]; // handle handy syntax for unary functions
@@ -30,6 +45,8 @@ function func(domain, codomain, name) {
   }
 
   var displayName = name || getDefaultName(domain, codomain);
+  var domainLength = domain.length;
+  var optionalArgumentsIndex = getOptionalArgumentsIndex(domain);
 
   function FuncType(value, curried) {
 
@@ -56,7 +73,7 @@ function func(domain, codomain, name) {
 
   FuncType.is = function (x) {
     return isInstrumented(x) &&
-      x.instrumentation.domain.length === domain.length &&
+      x.instrumentation.domain.length === domainLength &&
       x.instrumentation.domain.every(function (type, i) {
         return type === domain[i];
       }) &&
@@ -76,20 +93,24 @@ function func(domain, codomain, name) {
 
     function fn() {
       var args = Array.prototype.slice.call(arguments);
-      var len = curried ?
-        args.length :
-        domain.length;
-      var argsType = tuple(domain.slice(0, len));
+      var argsLength = args.length;
 
-      args = argsType(args); // type check arguments
+      if (process.env.NODE_ENV !== 'production') {
+        // type-check arguments
+        var tupleLength = curried ? argsLength : Math.max(argsLength, optionalArgumentsIndex);
+        tuple(domain.slice(0, tupleLength), 'arguments of function ' + displayName)(args);
+      }
 
-      if (len === domain.length) {
-        return create(codomain, f.apply(this, args));
+      if (curried && argsLength < domainLength) {
+        if (process.env.NODE_ENV !== 'production') {
+          assert(argsLength > 0, 'Invalid arguments.length = 0 for curried function ' + displayName);
+        }
+        var g = Function.prototype.bind.apply(f, [this].concat(args));
+        var newDomain = func(domain.slice(argsLength), codomain);
+        return newDomain.of(g, true);
       }
       else {
-        var g = Function.prototype.bind.apply(f, [this].concat(args));
-        var newdomain = func(domain.slice(len), codomain);
-        return newdomain.of(g, curried);
+        return create(codomain, f.apply(this, args));
       }
     }
 
@@ -110,4 +131,5 @@ function func(domain, codomain, name) {
 }
 
 func.getDefaultName = getDefaultName;
+func.getOptionalArgumentsIndex = getOptionalArgumentsIndex;
 module.exports = func;


Test patch:
diff --git a/test/es6.js b/test/es6.js
index 0e2ecd9..1a30fc2 100644
--- a/test/es6.js
+++ b/test/es6.js
@@ -80,7 +80,7 @@ describe('ES6 classes', function () {
     assert.deepEqual(f(c), 'Class');
     throwsWithMessage(function () {
       f(1);
-    }, '[tcomb] Invalid value 1 supplied to [Class]/0: Class');
+    }, '[tcomb] Invalid value 1 supplied to arguments of function (Class) => String/0: Class');
   });
 
 });
diff --git a/test/func.js b/test/func.js
index 681d314..388721f 100644
--- a/test/func.js
+++ b/test/func.js
@@ -36,14 +36,40 @@ describe('t.func(domain, codomain, [name])', function () {
 
       util.throwsWithMessage(function () {
         sum(1, 2, 3);
-      }, '[tcomb] Invalid value [\n  1,\n  2,\n  3\n] supplied to [Number, Number] (expected an array of length 2)');
+      }, '[tcomb] Invalid value [\n  1,\n  2,\n  3\n] supplied to arguments of function (Number, Number) => Number (expected an array of length 2)');
 
       util.throwsWithMessage(function () {
         sum('a', 2);
-      }, '[tcomb] Invalid value "a" supplied to [Number, Number]/0: Number');
+      }, '[tcomb] Invalid value "a" supplied to arguments of function (Number, Number) => Number/0: Number');
 
     });
 
+    it('should handle optional arguments', function () {
+      function Class(a) {
+        this.a = a;
+      }
+      assert.equal(t.func.getOptionalArgumentsIndex([t.Number, t.Number]), 2);
+      assert.equal(t.func.getOptionalArgumentsIndex([t.Number, t.maybe(t.Number)]), 1);
+      assert.equal(t.func.getOptionalArgumentsIndex([t.maybe(t.Number)]), 0);
+      assert.equal(t.func.getOptionalArgumentsIndex([t.Number, t.maybe(t.Number), t.Number]), 3);
+      assert.equal(t.func.getOptionalArgumentsIndex([]), 0);
+      assert.equal(t.func.getOptionalArgumentsIndex([Class]), 1);
+      assert.equal(t.func.getOptionalArgumentsIndex([Class, t.maybe(t.Number)]), 1);
+
+      var T = t.func([t.Number, t.maybe(t.Number)], t.Number);
+      var sum = T.of(function (a, b) {
+        if (t.Nil.is(b)) {
+          b = 2;
+        }
+        return a + b;
+      });
+      assert.equal(sum(1), 3);
+      assert.equal(sum(1, 2), 3);
+      util.throwsWithMessage(function () {
+        sum(1, 'a');
+      }, '[tcomb] Invalid value "a" supplied to arguments of function (Number, ?Number) => Number/1: ?Number');
+    });
+
     it('should check the return value', function () {
 
       var T = t.func([t.Number, t.Number], t.Number);
@@ -90,6 +116,16 @@ describe('t.func(domain, codomain, [name])', function () {
 
   describe('currying', function () {
 
+    it('should throw if no arguments are passed in', function () {
+      var Type = t.func([t.Number, t.Number, t.Number], t.Number);
+      var sum = Type(function (a, b, c) {
+        return a + b + c;
+      }, true);
+      util.throwsWithMessage(function () {
+        sum();
+      }, '[tcomb] Invalid arguments.length = 0 for curried function (Number, Number, Number) => Number');
+    });
+
     it('should curry functions', function () {
       var Type = t.func([t.Number, t.Number, t.Number], t.Number);
       var sum = Type(function (a, b, c) {
@@ -117,12 +153,12 @@ describe('t.func(domain, codomain, [name])', function () {
 
       util.throwsWithMessage(function () {
         sum('a');
-      }, '[tcomb] Invalid value "a" supplied to [Number]/0: Number');
+      }, '[tcomb] Invalid value "a" supplied to arguments of function (Number, Number) => Number/0: Number');
 
       util.throwsWithMessage(function () {
         var sum1 = sum(1);
         sum1('a');
-      }, '[tcomb] Invalid value "a" supplied to [Number]/0: Number');
+      }, '[tcomb] Invalid value "a" supplied to arguments of function (Number) => Number/0: Number');
 
     });
 
@@ -138,7 +174,7 @@ describe('t.func(domain, codomain, [name])', function () {
       assert.deepEqual(sum(1, 2, 3), 6);
       util.throwsWithMessage(function () {
         sum(1, 2);
-      }, '[tcomb] Invalid value [\n  1,\n  2\n] supplied to [Number, Number, Number] (expected an array of length 3)');
+      }, '[tcomb] Invalid value [\n  1,\n  2\n] supplied to arguments of function (Number, Number, Number) => Number (expected an array of length 3)');
     });
 
   });