Repository: plaid/sanctuary
Pull request #86 (2015-09-16 03:20:36)

either: add S.encaseEither
I've been meaning to add this function for some time. @lucamartinetti asked me today whether such a function exists, prompting this pull request.

My only question is which of the following is the best combination of names:

| `(* -> a) -> (* -> Maybe a)`  | `(* -> a) -> (* -> Either Error a)` |
| ----------------------------- | ----------------------------------- |
| `encase`                      | `encaseEither`                      |
| `encaseMaybe`                 | `encase`                            |
| `encaseMaybe`                 | `encaseEither`                      |

I don't have a strong inclination in any direction, so I'm inclined to leave `S.encase` alone. Does anyone have a strong preference for one of the other options?


davidchambers: Thinking about this further as I rode home this evening I reached the conclusion that `(* -> a) -> (* -> Either Error a)` is not sufficiently flexible. Consider Kleisli composition:

```haskell
f :: a -> Either x b
g :: b -> Either x c
h :: a -> Either x c
h = f >=> g
```

In order to compose `f` and `g`, both must return values of type `Either x _` for some type `x`. If instead `g` were to return values of type `Either y c`, say, `h` would not be well typed, since we would not know whether to expect a Left resulting from the application of `h` to contain an `x` or a `y`.

The function as it currently stands could be used in conjunction with functions which return values of type `Either Error _`, but not with functions which return values of type `Either String _`.

Instead, we could provide `encaseEither :: (Error -> a) -> (* -> b) -> (* -> Either a b)`. One could then use:

  - `S.encaseEither(R.identity)` to produce results of type `Either Error _`; or
  - `S.encaseEither(R.prop('message'))` to produce results of type `Either String _`.

davidchambers: I've updated the pull request to provide the more general function.

benperez: After some discussion with @davidchambers IRL I agree that we don't want to lock ourselves into always having to chain together functions that return `Either Error a`. I'm on board with the `R.I` approach until we figure out what the really common use cases for this are.

:church: 

Patch:
diff --git a/index.js b/index.js
index 00f0458..f0b9325 100644
--- a/index.js
+++ b/index.js
@@ -729,6 +729,8 @@
   //. applying `f` to the same arguments: if this succeeds, `g` returns Just
   //. the result; otherwise `g` returns Nothing.
   //.
+  //. See also [`encaseEither`](#encaseEither).
+  //.
   //. ```javascript
   //. > S.encase(eval)('1 + 1')
   //. Just(2)
@@ -1048,6 +1050,38 @@
     return either instanceof Left ? l(either.value) : r(either.value);
   });
 
+  //# encaseEither :: (Error -> a) -> (* -> b) -> (* -> Either a b)
+  //.
+  //. 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.
+  //.
+  //. See also [`encase`](#encase).
+  //.
+  //. ```javascript
+  //. > S.encaseEither(R.identity, Array)(0)
+  //. Right([])
+  //.
+  //. > S.encaseEither(R.identity, Array)(-1)
+  //. Left(RangeError: Invalid array length)
+  //.
+  //. > S.encaseEither(R.prop('message'), Array)(-1)
+  //. Left("Invalid array length")
+  //. ```
+  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));
+      }
+    });
+  });
+
   //# maybeToEither :: a -> Maybe b -> Either a b
   //.
   //. Takes a value of any type and a Maybe, and returns an Either.


Test patch:
diff --git a/test/index.js b/test/index.js
index 4975d9f..e1b45b5 100644
--- a/test/index.js
+++ b/test/index.js
@@ -13,11 +13,22 @@ var eq = function(actual, expected) {
   assert.strictEqual(R.toString(actual), R.toString(expected));
 };
 
-//  errorEq :: Function -> String -> Error -> Boolean
+//  errorEq :: TypeRep a -> String -> Error -> Boolean
 var errorEq = R.curry(function(type, message, error) {
   return error.constructor === type && error.message === message;
 });
 
+//  factorial :: Number -> Number !
+var factorial = function(n) {
+  if (n < 0) {
+    throw new Error('Cannot determine factorial of negative number');
+  } else if (n === 0) {
+    return 1;
+  } else {
+    return n * factorial(n - 1);
+  }
+};
+
 //  parseHex :: String -> Either String Number
 var parseHex = function(s) {
   var n = parseInt(s, 16);
@@ -757,17 +768,6 @@ describe('maybe', function() {
                             'as its first argument; received [1, 2, 3]'));
     });
 
-    //  factorial :: Number -> Number
-    var factorial = function(n) {
-      if (n < 0) {
-        throw new Error('Cannot determine factorial of negative number');
-      } else if (n === 0) {
-        return 1;
-      } else {
-        return n * factorial(n - 1);
-      }
-    };
-
     it('returns a function which returns a Just on success', function() {
       eq(S.encase(factorial)(5), S.Just(120));
     });
@@ -779,6 +779,7 @@ describe('maybe', function() {
     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));
     });
 
     it('returns a function of appropriate arity', function() {
@@ -1255,6 +1256,67 @@ describe('either', function() {
 
   });
 
+  describe('encaseEither', function() {
+
+    it('is a binary function', function() {
+      eq(typeof S.encaseEither, 'function');
+      eq(S.encaseEither.length, 2);
+    });
+
+    it('type checks its arguments', function() {
+      assert.throws(function() { S.encaseEither(null); },
+                    errorEq(TypeError,
+                            '‘encaseEither’ requires a value of type ' +
+                            'Function as its first argument; received null'));
+
+      assert.throws(function() { S.encaseEither(R.identity, 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 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('applies the first argument to the Error', function() {
+      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));
+    });
+
+    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('preserves context', function() {
+      var f = S.encaseEither(R.identity, function() { return this; });
+      eq(f.call({foo: 42}), S.Right({foo: 42}));
+    });
+
+    it('is curried', function() {
+      eq(S.encaseEither(R.identity).length, 1);
+      eq(S.encaseEither(R.identity)(factorial)(5), S.Right(120));
+    });
+
+  });
+
   describe('maybeToEither', function() {
 
     it('is a binary function', function() {