Repository: plaid/sanctuary (js)

Issue:
simplify S.encase and S.encaseEither
Currently, [`encase`][1] and [`encaseEither`][2] have misleading type signatures:

```haskell
encase :: (* -> a) -> (* -> Maybe a)

encaseEither :: (Error -> a) -> (* -> b) -> (* -> Either a b)
```

They are misleading because Hindley–Milner notation cannot represent a function which returns a curried function of unknown arity.

In addition to the documentation problem, there's a usability problem. Functions with unwanted optional arguments must be wrapped in lambdas. For example:

```javascript
S.encaseEither(S.I, x => JSON.parse(x))
```

A solution to both problems is to have one function for each arity up to some arbitrary limit:

```haskell
encase  :: (a -> b) -> a -> Maybe b
encase2 :: (a -> b -> c) -> a -> b -> Maybe c
encase3 :: (a -> b -> c -> d) -> a -> b -> c -> Maybe d

encaseEither  :: (Error -> l) -> (a -> r) -> a -> Either l r
encaseEither2 :: (Error -> l) -> (a -> b -> r) -> a -> b -> Either l r
encaseEither3 :: (Error -> l) -> (a -> b -> c -> r) -> a -> b -> c -> Either l r
```

An incidental benefit of this approach is that arguments to the wrapped function may be provided up front. For example:

```javascript
> S.encaseEither(R.prop('message'), JSON.parse, '[1,2,3]')
Right([1, 2, 3])
> S.encaseEither(R.prop('message'), JSON.parse, 'XXX')
Left("Unexpected token X")
```


[1]: https://github.com/plaid/sanctuary#encase
[2]: https://github.com/plaid/sanctuary#encaseEither


Pull request description:
maybe, either: simplify S.encase and S.encaseEither
Closes #103


Patch:
diff --git a/index.js b/index.js
index e2b7a77..4c6bdc6 100644
--- a/index.js
+++ b/index.js
@@ -150,7 +150,7 @@
                        R.nth(_, ['zero', 'one', 'two', 'three', 'four', 'five',
                                  'six', 'seven', 'eight', 'nine', 'ten']),
                        String),
-    '{ord}': R.nth(_, ['first', 'second', 'third', 'fourth']),
+    '{ord}': R.nth(_, ['first', 'second', 'third', 'fourth', 'fifth']),
     '{quote}': function(s) { return '\u2018' + s + '\u2019'; },
     '{repr}': R.toString,
     '{type}': functionName
@@ -213,6 +213,7 @@
       case 2: return function(a, b) { return f.apply(this, arguments); };
       case 3: return function(a, b, c) { return f.apply(this, arguments); };
       case 4: return function(a, b, c, d) { return f.apply(this, arguments); };
+      case 5: return function(a, b, c, d, e) { return f.apply(this, arguments); };
     }
   };
 
@@ -988,30 +989,50 @@
   //. ```
   S.mapMaybe = def('mapMaybe', [Function, List], R.compose(catMaybes, R.map));
 
-  //# encase :: (* -> a) -> (* -> Maybe a)
+  //# encase :: (a -> b) -> a -> Maybe b
   //.
-  //. Takes a function `f` which may throw and returns a curried function
-  //. `g` which will not throw. The result of applying `g` is determined by
-  //. applying `f` to the same arguments: if this succeeds, `g` returns Just
-  //. the result; otherwise `g` returns Nothing.
+  //. Takes a unary function `f` which may throw and a value `x` of any type,
+  //. and applies `f` to `x` inside a `try` block. If an exception is caught,
+  //. the return value is a Nothing; otherwise the return value is Just the
+  //. result of applying `f` to `x`.
   //.
   //. See also [`encaseEither`](#encaseEither).
   //.
   //. ```javascript
-  //. > S.encase(eval)('1 + 1')
+  //. > S.encase(eval, '1 + 1')
   //. Just(2)
   //.
-  //. > S.encase(eval)('1 +')
+  //. > S.encase(eval, '1 +')
   //. Nothing()
   //. ```
-  var encase = S.encase = def('encase', [Function], function(f) {
-    return R.curryN(f.length, function() {
-      try {
-        return Just(f.apply(this, arguments));
-      } catch (err) {
-        return Nothing();
-      }
-    });
+  var encase = S.encase = def('encase', [Function, a], function(f, x) {
+    try {
+      return Just(f(x));
+    } catch (err) {
+      return Nothing();
+    }
+  });
+
+  //# encase2 :: (a -> b -> c) -> a -> b -> Maybe c
+  //.
+  //. Binary version of [`encase`](#encase).
+  S.encase2 = def('encase2', [Function, a, b], function(f, x, y) {
+    try {
+      return Just(f(x, y));
+    } catch (err) {
+      return Nothing();
+    }
+  });
+
+  //# encase3 :: (a -> b -> c -> d) -> a -> b -> c -> Maybe d
+  //.
+  //. Ternary version of [`encase`](#encase).
+  S.encase3 = def('encase3', [Function, a, b, c], function(f, x, y, z) {
+    try {
+      return Just(f(x, y, z));
+    } catch (err) {
+      return Nothing();
+    }
   });
 
   //. ### Either type
@@ -1400,36 +1421,57 @@
     return either.isLeft ? l(either.value) : r(either.value);
   });
 
-  //# encaseEither :: (Error -> a) -> (* -> b) -> (* -> Either a b)
+  //# encaseEither :: (Error -> l) -> (a -> r) -> a -> Either l r
   //.
-  //. Takes two functions, `f` and `g`, the second of which may throw,
-  //. and returns a curried function of the same arity as `g` which will
-  //. not throw. The result of applying this function is determined by
-  //. applying `g` to the same arguments: if this succeeds, the return
-  //. value is a Right whose value is the result; otherwise the return
-  //. value is a Left whose value is the result of applying `f` to the
-  //. caught Error object.
+  //. Takes two unary functions, `f` and `g`, the second of which may throw,
+  //. and a value `x` of any type. Applies `g` to `x` inside a `try` block.
+  //. If an exception is caught, the return value is a Left containing the
+  //. result of applying `f` to the caught Error object; otherwise the return
+  //. value is a Right containing the result of applying `g` to `x`.
   //.
   //. See also [`encase`](#encase).
   //.
   //. ```javascript
-  //. > S.encaseEither(R.identity, Array)(0)
-  //. Right([])
+  //. > S.encaseEither(S.I, JSON.parse, '["foo","bar","baz"]')
+  //. Right(['foo', 'bar', 'baz'])
   //.
-  //. > S.encaseEither(R.identity, Array)(-1)
-  //. Left(new RangeError('Invalid array length'))
+  //. > S.encaseEither(S.I, JSON.parse, '[')
+  //. Left(new SyntaxError('Unexpected end of input'))
   //.
-  //. > S.encaseEither(R.prop('message'), Array)(-1)
-  //. Left('Invalid array length')
+  //. > S.encaseEither(R.prop('message'), JSON.parse, '[')
+  //. Left('Unexpected end of input')
   //. ```
-  S.encaseEither = def('encaseEither', [Function, Function], function(f, g) {
-    return R.curryN(g.length, function() {
-      try {
-        return Right(g.apply(this, arguments));
-      } catch (err) {
-        return Left(f(err));
-      }
-    });
+  S.encaseEither =
+  def('encaseEither', [Function, Function, a], function(f, g, x) {
+    try {
+      return Right(g(x));
+    } catch (err) {
+      return Left(f(err));
+    }
+  });
+
+  //# encaseEither2 :: (Error -> l) -> (a -> b -> r) -> a -> b -> Either l r
+  //.
+  //. Binary version of [`encaseEither`](#encaseEither).
+  S.encaseEither2 =
+  def('encaseEither2', [Function, Function, a, b], function(f, g, x, y) {
+    try {
+      return Right(g(x, y));
+    } catch (err) {
+      return Left(f(err));
+    }
+  });
+
+  //# encaseEither3 :: (Error -> l) -> (a -> b -> c -> r) -> a -> b -> c -> Either l r
+  //.
+  //. Ternary version of [`encaseEither`](#encaseEither).
+  S.encaseEither3 =
+  def('encaseEither3', [Function, Function, a, b, c], function(f, g, x, y, z) {
+    try {
+      return Right(g(x, y, z));
+    } catch (err) {
+      return Left(f(err));
+    }
   });
 
   //# maybeToEither :: a -> Maybe b -> Either a b
@@ -2048,9 +2090,7 @@
   //. > S.parseJson('[')
   //. Nothing()
   //. ```
-  S.parseJson = def('parseJson', [String], encase(function(s) {
-    return JSON.parse(s);
-  }));
+  S.parseJson = def('parseJson', [String], encase(JSON.parse));
 
   //. ### RegExp
 


Test patch:
diff --git a/test/index.js b/test/index.js
index 4da0b22..c3880fc 100644
--- a/test/index.js
+++ b/test/index.js
@@ -21,6 +21,16 @@ var errorEq = R.curry(function(type, message, error) {
   return error.constructor === type && error.message === message;
 });
 
+//  area :: (Number, Number, Number) -> Number !
+var area = function(a, b, c) {
+  if (Math.max(a, b, c) < (a + b + c) / 2) {
+    var s = (a + b + c) / 2;
+    return Math.sqrt(s * (s - a) * (s - b) * (s - c));
+  } else {
+    throw new Error('Impossible triangle');
+  }
+};
+
 //  factorial :: Number -> Number !
 var factorial = function(n) {
   if (n < 0) {
@@ -38,6 +48,15 @@ var parseHex = function(s) {
   return n !== n ? S.Left('Invalid hexadecimal string') : S.Right(n);
 };
 
+//  rem :: (Number, Number) -> Number !
+var rem = function(x, y) {
+  if (y === 0) {
+    throw new Error('Cannot divide by zero');
+  } else {
+    return x % y;
+  }
+};
+
 //  square :: Number -> Number
 var square = function(n) { return n * n; };
 
@@ -1017,9 +1036,9 @@ describe('maybe', function() {
 
   describe('encase', function() {
 
-    it('is a unary function', function() {
+    it('is a binary function', function() {
       eq(typeof S.encase, 'function');
-      eq(S.encase.length, 1);
+      eq(S.encase.length, 2);
     });
 
     it('type checks its arguments', function() {
@@ -1029,28 +1048,90 @@ describe('maybe', function() {
                             'as its first argument; received [1, 2, 3]'));
     });
 
-    it('returns a function which returns a Just on success', function() {
+    it('returns a Just on success', function() {
+      eq(S.encase(factorial, 5), S.Just(120));
+    });
+
+    it('returns a Nothing on failure', function() {
+      eq(S.encase(factorial, -1), S.Nothing());
+    });
+
+    it('can be applied to a function of arbitrary arity', function() {
+      eq(S.encase(function(a, b, c, d) { return a; }, 42), S.Just(42));
+    });
+
+    it('is curried', function() {
+      eq(S.encase(factorial).length, 1);
       eq(S.encase(factorial)(5), S.Just(120));
     });
 
-    it('returns a function which returns a Nothing on failure', function() {
-      eq(S.encase(factorial)(-1), S.Nothing());
+  });
+
+  describe('encase2', function() {
+
+    it('is a ternary function', function() {
+      eq(typeof S.encase2, 'function');
+      eq(S.encase2.length, 3);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.encase2([1, 2, 3]); },
+                    errorEq(TypeError,
+                            '‘encase2’ requires a value of type Function ' +
+                            'as its first argument; received [1, 2, 3]'));
+    });
+
+    it('returns a Just on success', function() {
+      eq(S.encase2(rem, 42, 5), S.Just(2));
+    });
+
+    it('returns a Nothing on failure', function() {
+      eq(S.encase2(rem, 42, 0), S.Nothing());
     });
 
     it('can be applied to a function of arbitrary arity', function() {
-      var f = S.encase(function(a, b, c, d, e) { return e; });
-      eq(f(1, 2, 3, 4, 5), S.Just(5));
-      eq(f(1)(2)(3)(4)(5), S.Just(5));
+      eq(S.encase2(function(a, b, c, d) { return b; }, 0, 42), S.Just(42));
     });
 
-    it('returns a function of appropriate arity', function() {
-      var f = S.encase(function(a, b, c, d, e) { return e; });
-      eq(f.length, 5);
+    it('is curried', function() {
+      eq(S.encase2(rem).length, 2);
+      eq(S.encase2(rem)(42).length, 1);
+      eq(S.encase2(rem)(42)(5), S.Just(2));
     });
 
-    it('preserves context', function() {
-      var f = S.encase(function() { return this; });
-      eq(f.call({foo: 42}), S.Just({foo: 42}));
+  });
+
+  describe('encase3', function() {
+
+    it('is a quaternary function', function() {
+      eq(typeof S.encase3, 'function');
+      eq(S.encase3.length, 4);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.encase3([1, 2, 3]); },
+                    errorEq(TypeError,
+                            '‘encase3’ requires a value of type Function ' +
+                            'as its first argument; received [1, 2, 3]'));
+    });
+
+    it('returns a Just on success', function() {
+      eq(S.encase3(area, 3, 4, 5), S.Just(6));
+    });
+
+    it('returns a Nothing on failure', function() {
+      eq(S.encase3(area, 2, 2, 5), S.Nothing());
+    });
+
+    it('can be applied to a function of arbitrary arity', function() {
+      eq(S.encase3(function(a, b, c, d) { return c; }, 0, 0, 42), S.Just(42));
+    });
+
+    it('is curried', function() {
+      eq(S.encase3(area).length, 3);
+      eq(S.encase3(area)(3).length, 2);
+      eq(S.encase3(area)(3)(4).length, 1);
+      eq(S.encase3(area)(3)(4)(5), S.Just(6));
     });
 
   });
@@ -1631,9 +1712,9 @@ describe('either', function() {
 
   describe('encaseEither', function() {
 
-    it('is a binary function', function() {
+    it('is a ternary function', function() {
       eq(typeof S.encaseEither, 'function');
-      eq(S.encaseEither.length, 2);
+      eq(S.encaseEither.length, 3);
     });
 
     it('type checks its arguments', function() {
@@ -1642,50 +1723,130 @@ describe('either', function() {
                             '‘encaseEither’ requires a value of type ' +
                             'Function as its first argument; received null'));
 
-      assert.throws(function() { S.encaseEither(R.identity, null); },
+      assert.throws(function() { S.encaseEither(S.I, null); },
                     errorEq(TypeError,
                             '‘encaseEither’ requires a value of type ' +
                             'Function as its second argument; received null'));
     });
 
-    it('returns a function which returns a Right on success', function() {
-      eq(S.encaseEither(R.identity, factorial)(5), S.Right(120));
+    it('returns a Right on success', function() {
+      eq(S.encaseEither(S.I, factorial, 5), S.Right(120));
     });
 
-    it('returns a function which returns a Left on failure', function() {
-      var result = S.encaseEither(R.identity, factorial)(-1);
-      assert(result instanceof S.Left);
-      assert(errorEq(Error,
-                     'Cannot determine factorial of negative number',
-                     result.value));
+    it('returns a Left on failure', function() {
+      eq(S.encaseEither(S.I, factorial, -1),
+         S.Left(new Error('Cannot determine factorial of negative number')));
     });
 
     it('applies the first argument to the Error', function() {
-      eq(S.encaseEither(R.prop('message'), factorial)(-1),
+      eq(S.encaseEither(R.prop('message'), factorial, -1),
          S.Left('Cannot determine factorial of negative number'));
     });
 
     it('can be applied to a function of arbitrary arity', function() {
-      var f = S.encaseEither(R.identity,
-                             function(a, b, c, d, e) { return e; });
-      eq(f(1, 2, 3, 4, 5), S.Right(5));
-      eq(f(1)(2)(3)(4)(5), S.Right(5));
+      eq(S.encaseEither(S.I, function(a, b, c, d) { return a; }, 42),
+         S.Right(42));
+    });
+
+    it('is curried', function() {
+      eq(S.encaseEither(S.I).length, 2);
+      eq(S.encaseEither(S.I)(factorial).length, 1);
+      eq(S.encaseEither(S.I)(factorial)(5), S.Right(120));
+    });
+
+  });
+
+  describe('encaseEither2', function() {
+
+    it('is a quaternary function', function() {
+      eq(typeof S.encaseEither2, 'function');
+      eq(S.encaseEither2.length, 4);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.encaseEither2(null); },
+                    errorEq(TypeError,
+                            '‘encaseEither2’ requires a value of type ' +
+                            'Function as its first argument; received null'));
+
+      assert.throws(function() { S.encaseEither2(S.I, null); },
+                    errorEq(TypeError,
+                            '‘encaseEither2’ requires a value of type ' +
+                            'Function as its second argument; received null'));
     });
 
-    it('returns a function of appropriate arity', function() {
-      var f = S.encaseEither(R.identity,
-                             function(a, b, c, d, e) { return e; });
-      eq(f.length, 5);
+    it('returns a Right on success', function() {
+      eq(S.encaseEither2(S.I, rem, 42, 5), S.Right(2));
     });
 
-    it('preserves context', function() {
-      var f = S.encaseEither(R.identity, function() { return this; });
-      eq(f.call({foo: 42}), S.Right({foo: 42}));
+    it('returns a Left on failure', function() {
+      eq(S.encaseEither2(S.I, rem, 42, 0),
+         S.Left(new Error('Cannot divide by zero')));
+    });
+
+    it('applies the first argument to the Error', function() {
+      eq(S.encaseEither2(R.prop('message'), rem, 42, 0),
+         S.Left('Cannot divide by zero'));
+    });
+
+    it('can be applied to a function of arbitrary arity', function() {
+      eq(S.encaseEither2(S.I, function(a, b, c, d) { return b; }, 0, 42),
+         S.Right(42));
+    });
+
+    it('is curried', function() {
+      eq(S.encaseEither2(S.I).length, 3);
+      eq(S.encaseEither2(S.I)(rem).length, 2);
+      eq(S.encaseEither2(S.I)(rem)(42).length, 1);
+      eq(S.encaseEither2(S.I)(rem)(42)(5), S.Right(2));
+    });
+
+  });
+
+  describe('encaseEither3', function() {
+
+    it('is a quinary function', function() {
+      eq(typeof S.encaseEither3, 'function');
+      eq(S.encaseEither3.length, 5);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.encaseEither3(null); },
+                    errorEq(TypeError,
+                            '‘encaseEither3’ requires a value of type ' +
+                            'Function as its first argument; received null'));
+
+      assert.throws(function() { S.encaseEither3(S.I, null); },
+                    errorEq(TypeError,
+                            '‘encaseEither3’ requires a value of type ' +
+                            'Function as its second argument; received null'));
+    });
+
+    it('returns a Right on success', function() {
+      eq(S.encaseEither3(S.I, area, 3, 4, 5), S.Right(6));
+    });
+
+    it('returns a Left on failure', function() {
+      eq(S.encaseEither3(S.I, area, 2, 2, 5),
+         S.Left(new Error('Impossible triangle')));
+    });
+
+    it('applies the first argument to the Error', function() {
+      eq(S.encaseEither3(R.prop('message'), area, 2, 2, 5),
+         S.Left('Impossible triangle'));
+    });
+
+    it('can be applied to a function of arbitrary arity', function() {
+      eq(S.encaseEither3(S.I, function(a, b, c, d) { return c; }, 0, 0, 42),
+         S.Right(42));
     });
 
     it('is curried', function() {
-      eq(S.encaseEither(R.identity).length, 1);
-      eq(S.encaseEither(R.identity)(factorial)(5), S.Right(120));
+      eq(S.encaseEither3(S.I).length, 4);
+      eq(S.encaseEither3(S.I)(area).length, 3);
+      eq(S.encaseEither3(S.I)(area)(3).length, 2);
+      eq(S.encaseEither3(S.I)(area)(3)(4).length, 1);
+      eq(S.encaseEither3(S.I)(area)(3)(4)(5), S.Right(6));
     });
 
   });