Repository: plaid/sanctuary
Pull request #27 (2015-05-09 01:30:39)

either: implement Apply, Applicative, Chain, and Monad
I *think* this is correct, but I'd greatly appreciate a review.



Patch:
diff --git a/index.js b/index.js
index ea00f78..06f5c15 100644
--- a/index.js
+++ b/index.js
@@ -100,16 +100,16 @@
     return maybe instanceof Nothing;
   };
 
+  //  Nothing#or :: Maybe a -> Maybe a
+  Nothing.prototype.or = function(maybe) {
+    return maybe;
+  };
+
   //  Nothing#map :: (a -> b) -> Maybe b
   Nothing.prototype.map = function(f) {  // jshint ignore:line
     return this;
   };
 
-  //  Nothing#toBoolean :: -> Boolean
-  Nothing.prototype.toBoolean = function() {
-    return false;
-  };
-
   var Just = S.Just = function Just(value) {
     if (!(this instanceof Just)) {
       return new Just(value);
@@ -139,16 +139,16 @@
     return maybe instanceof Just && maybe.value === this.value;
   };
 
+  //  Just#or :: Maybe a -> Maybe a
+  Just.prototype.or = function(maybe) {  // jshint ignore:line
+    return this;
+  };
+
   //  Just#map :: (a -> b) -> Maybe b
   Just.prototype.map = function(f) {
     return new Just(f(this.value));
   };
 
-  //  Just#toBoolean :: -> Boolean
-  Just.prototype.toBoolean = function() {
-    return true;
-  };
-
   //  fromMaybe :: a -> Maybe a -> a
   S.fromMaybe = R.curry(function(x, maybe) {
     switch (true) {
@@ -181,6 +181,13 @@
     throw new Error('Cannot instantiate Either');
   };
 
+  //  Either.of :: b -> m a b
+  Either.of = function(x) {
+    return new Right(x);
+  };
+
+  Either.prototype.of = Either.of;
+
   Either.prototype.type = Either;
 
   var Left = S.Left = function Left(value) {
@@ -191,6 +198,16 @@
   };
   extend(Left, Either);
 
+  //  Left#ap :: m a -> m b
+  Left.prototype.ap = function(x) {  // jshint ignore:line
+    return this;
+  };
+
+  //  Left#chain :: (a -> m b) -> m b
+  Left.prototype.chain = function(f) {  // jshint ignore:line
+    return this;
+  };
+
   //  Left#equals :: Either a b -> Boolean
   Left.prototype.equals = function(either) {
     return either instanceof Left && either.value === this.value;
@@ -209,6 +226,16 @@
   };
   extend(Right, Either);
 
+  //  Right#ap :: m a -> m b
+  Right.prototype.ap = function(x) {
+    return x.map(this.value);
+  };
+
+  //  Right#chain :: (a -> m b) -> m b
+  Right.prototype.chain = function(f) {
+    return f(this.value);
+  };
+
   //  Right#equals :: Either a b -> Boolean
   Right.prototype.equals = function(either) {
     return either instanceof Right && either.value === this.value;
@@ -233,41 +260,16 @@
 
   //  control  ///////////////////////////////////////////////////////////////
 
-  var assertTypeMatch = function(x, y) {
+  //  or :: f a -> f a -> f a
+  S.or = R.curry(function(x, y) {
     if (R.type(x) !== R.type(y) || x.type !== y.type) {
       throw new TypeError('Type mismatch');
-    }
-  };
-
-  //  toBoolean :: * -> Boolean
-  var toBoolean = function(x) {
-    switch (true) {
-      case R.is(Array, x):    return x.length > 0;
-      case R.is(Boolean, x):  return x;
-      default:                return x.toBoolean();
-    }
-  };
-
-  //  and :: a -> a -> a
-  S.and = R.curry(function(x, y) {
-    assertTypeMatch(x, y);
-    return toBoolean(x) ? y : x;
-  });
-
-  //  or :: a -> a -> a
-  var or = S.or = R.curry(function(x, y) {
-    assertTypeMatch(x, y);
-    return toBoolean(x) ? x : y;
-  });
-
-  //  xor :: a -> a -> a
-  S.xor = R.curry(function(x, y) {
-    assertTypeMatch(x, y);
-    switch (true) {
-      case toBoolean(x) !== toBoolean(y): return or(x, y);
-      case R.is(Array, x):                return [];
-      case R.is(Boolean, x):              return false;
-      default:                            return x.empty();
+    } else if (typeof x.or === 'function') {
+      return x.or(y);
+    } else if (R.is(Array, x)) {
+      return x.length > 0 ? x : y;
+    } else {
+      throw new TypeError('"or" unspecified for ' + x.constructor.name);
     }
   });
 


Test patch:
diff --git a/test/index.js b/test/index.js
index 4097c25..891123f 100644
--- a/test/index.js
+++ b/test/index.js
@@ -11,9 +11,21 @@ var S = require('..');
 
 var eq = assert.strictEqual;
 
+//  parseHex :: String -> Either String Number
+var parseHex = function(s) {
+  var n = parseInt(s, 16);
+  return n !== n ? S.Left('Invalid hexadecimal string') : S.Right(n);
+};
+
 //  square :: Number -> Number
 var square = function(n) { return n * n; };
 
+//  squareRoot :: Number -> Either String Number
+var squareRoot = function(n) {
+  return n < 0 ? S.Left('Cannot represent square root of negative number')
+               : S.Right(Math.sqrt(n));
+};
+
 
 describe('invariants', function() {
 
@@ -150,10 +162,12 @@ describe('maybe', function() {
       eq(nothing.map(function() { return 42; }), nothing);
     });
 
-    it('provides a "toBoolean" method', function() {
+    it('provides an "or" method', function() {
       var nothing = S.Nothing();
-      eq(nothing.toBoolean.length, 0);
-      eq(nothing.toBoolean(), false);
+      var just = S.Just(42);
+      eq(S.Nothing().or.length, 1);
+      eq(S.Nothing().or(nothing), nothing);
+      eq(S.Nothing().or(just), just);
     });
 
     it('implements Semigroup', function() {
@@ -318,10 +332,11 @@ describe('maybe', function() {
       assert(just.map(function(x) { return x / 2; }).equals(S.Just(21)));
     });
 
-    it('provides a "toBoolean" method', function() {
+    it('provides an "or" method', function() {
       var just = S.Just(42);
-      eq(just.toBoolean.length, 0);
-      eq(just.toBoolean(), true);
+      eq(just.or.length, 1);
+      eq(just.or(S.Nothing()), just);
+      eq(just.or(S.Just(42)), just);
     });
 
     it('implements Semigroup', function() {
@@ -543,6 +558,19 @@ describe('either', function() {
       eq(left.type, S.Either);
     });
 
+    it('provides an "ap" method', function() {
+      var left = S.Left('abc');
+      eq(left.ap.length, 1);
+      assert(left.ap(S.Left('xyz')).equals(S.Left('abc')));
+      assert(left.ap(S.Right(42)).equals(S.Left('abc')));
+    });
+
+    it('provides a "chain" method', function() {
+      var left = S.Left('abc');
+      eq(left.chain.length, 1);
+      eq(left.chain(squareRoot), left);
+    });
+
     it('provides an "equals" method', function() {
       var left = S.Left(42);
       eq(left.equals.length, 1);
@@ -559,6 +587,71 @@ describe('either', function() {
       eq(left.map(square), left);
     });
 
+    it('implements Functor', function() {
+      var a = S.Left('Cannot divide by zero');
+      var f = R.inc;
+      var g = square;
+
+      // identity
+      assert(a.map(R.identity).equals(a));
+
+      // composition
+      assert(a.map(function(x) { return f(g(x)); }).equals(a.map(g).map(f)));
+    });
+
+    it('implements Apply', function() {
+      var a = S.Left('Cannot divide by zero');
+      var b = S.Left('Cannot divide by zero');
+      var c = S.Left('Cannot divide by zero');
+
+      // composition
+      assert(a.map(function(f) {
+        return function(g) {
+          return function(x) {
+            return f(g(x));
+          };
+        };
+      }).ap(b).ap(c).equals(a.ap(b.ap(c))));
+    });
+
+    it('implements Applicative', function() {
+      var a = S.Left('Cannot divide by zero');
+      var b = S.Left('Cannot divide by zero');
+      var f = R.inc;
+      var x = 7;
+
+      // identity
+      assert(a.of(R.identity).ap(b).equals(b));
+
+      // homomorphism
+      assert(a.of(f).ap(a.of(x)).equals(a.of(f(x))));
+
+      // interchange
+      assert(a.of(function(f) { return f(x); }).ap(b).equals(b.ap(a.of(x))));
+    });
+
+    it('implements Chain', function() {
+      var a = S.Left('Cannot divide by zero');
+      var f = parseHex;
+      var g = squareRoot;
+
+      // associativity
+      assert(a.chain(f).chain(g)
+             .equals(a.chain(function(x) { return f(x).chain(g); })));
+    });
+
+    it('implements Monad', function() {
+      var a = S.Left('Cannot divide by zero');
+      var f = squareRoot;
+      var x = 25;
+
+      // left identity
+      assert(a.of(x).chain(f).equals(f(x)));
+
+      // right identity
+      assert(a.chain(a.of).equals(a));
+    });
+
   });
 
   describe('Right', function() {
@@ -582,6 +675,19 @@ describe('either', function() {
       eq(right.type, S.Either);
     });
 
+    it('provides an "ap" method', function() {
+      var right = S.Right(R.inc);
+      eq(right.ap.length, 1);
+      assert(right.ap(S.Left('abc')).equals(S.Left('abc')));
+      assert(right.ap(S.Right(42)).equals(S.Right(43)));
+    });
+
+    it('provides a "chain" method', function() {
+      var right = S.Right(25);
+      eq(right.chain.length, 1);
+      assert(right.chain(squareRoot).equals(S.Right(5)));
+    });
+
     it('provides an "equals" method', function() {
       var right = S.Right(42);
       eq(right.equals.length, 1);
@@ -598,6 +704,71 @@ describe('either', function() {
       assert(right.map(square).equals(S.Right(1764)));
     });
 
+    it('implements Functor', function() {
+      var a = S.Right(7);
+      var f = R.inc;
+      var g = square;
+
+      // identity
+      assert(a.map(R.identity).equals(a));
+
+      // composition
+      assert(a.map(function(x) { return f(g(x)); }).equals(a.map(g).map(f)));
+    });
+
+    it('implements Apply', function() {
+      var a = S.Right(R.inc);
+      var b = S.Right(square);
+      var c = S.Right(7);
+
+      // composition
+      assert(a.map(function(f) {
+        return function(g) {
+          return function(x) {
+            return f(g(x));
+          };
+        };
+      }).ap(b).ap(c).equals(a.ap(b.ap(c))));
+    });
+
+    it('implements Applicative', function() {
+      var a = S.Right(null);
+      var b = S.Right(R.inc);
+      var f = R.inc;
+      var x = 7;
+
+      // identity
+      assert(a.of(R.identity).ap(b).equals(b));
+
+      // homomorphism
+      assert(a.of(f).ap(a.of(x)).equals(a.of(f(x))));
+
+      // interchange
+      assert(a.of(function(f) { return f(x); }).ap(b).equals(b.ap(a.of(x))));
+    });
+
+    it('implements Chain', function() {
+      var a = S.Right('0x0100');
+      var f = parseHex;
+      var g = squareRoot;
+
+      // associativity
+      assert(a.chain(f).chain(g)
+             .equals(a.chain(function(x) { return f(x).chain(g); })));
+    });
+
+    it('implements Monad', function() {
+      var a = S.Right(null);
+      var f = squareRoot;
+      var x = 25;
+
+      // left identity
+      assert(a.of(x).chain(f).equals(f(x)));
+
+      // right identity
+      assert(a.chain(a.of).equals(a));
+    });
+
   });
 
   describe('either', function() {
@@ -637,86 +808,23 @@ describe('either', function() {
 
 describe('control', function() {
 
-  var empty = [];
-  var empty2 = [];
-  var nonempty = [42];
-  var nonempty2 = [42];
-
-  var nothing = S.Nothing();
-  var nothing2 = S.Nothing();
-  var just = S.Just(42);
-  var just2 = S.Just(42);
-
-  describe('and', function() {
-
-    it('is a binary function', function() {
-      eq(typeof S.and, 'function');
-      eq(S.and.length, 2);
-    });
-
-    it('can be applied to Booleans', function() {
-      eq(S.and(false, false), false);
-      eq(S.and(false, true), false);
-      eq(S.and(true, false), false);
-      eq(S.and(true, true), true);
-    });
-
-    it('can be applied to arrays', function() {
-      eq(S.and(empty, empty2), empty);
-      eq(S.and(empty, nonempty), empty);
-      eq(S.and(nonempty, empty), empty);
-      eq(S.and(nonempty, nonempty2), nonempty2);
-    });
-
-    it('can be applied to maybes', function() {
-      eq(S.and(nothing, nothing2), nothing);
-      eq(S.and(nothing, just), nothing);
-      eq(S.and(just, nothing), nothing);
-      eq(S.and(just, just2), just2);
-    });
-
-    it('throws if applied to values of different types', function() {
-      function Foo() {}
-      Foo.prototype.type = Foo;
-      var foo = new Foo();
-
-      assert.throws(
-        function() { S.and(empty, nothing); },
-        function(err) {
-          return err instanceof TypeError &&
-                 err.message === 'Type mismatch';
-        }
-      );
-
-      assert.throws(
-        function() { S.and(nothing, foo); },
-        function(err) {
-          return err instanceof TypeError &&
-                 err.message === 'Type mismatch';
-        }
-      );
-    });
-
-    it('is curried', function() {
-      eq(S.and(empty)(nonempty), empty);
-    });
+  describe('or', function() {
 
-  });
+    var empty = [];
+    var empty2 = [];
+    var nonempty = [42];
+    var nonempty2 = [42];
 
-  describe('or', function() {
+    var nothing = S.Nothing();
+    var nothing2 = S.Nothing();
+    var just = S.Just(42);
+    var just2 = S.Just(42);
 
     it('is a binary function', function() {
       eq(typeof S.or, 'function');
       eq(S.or.length, 2);
     });
 
-    it('can be applied to Booleans', function() {
-      eq(S.or(false, false), false);
-      eq(S.or(false, true), true);
-      eq(S.or(true, false), true);
-      eq(S.or(true, true), true);
-    });
-
     it('can be applied to arrays', function() {
       eq(S.or(empty, empty2), empty2);
       eq(S.or(empty, nonempty), nonempty);
@@ -753,64 +861,18 @@ describe('control', function() {
       );
     });
 
-    it('is curried', function() {
-      eq(S.or(empty)(nonempty), nonempty);
-    });
-
-  });
-
-  describe('xor', function() {
-
-    it('is a binary function', function() {
-      eq(typeof S.xor, 'function');
-      eq(S.xor.length, 2);
-    });
-
-    it('can be applied to Booleans', function() {
-      eq(S.xor(false, false), false);
-      eq(S.xor(false, true), true);
-      eq(S.xor(true, false), true);
-      eq(S.xor(true, true), false);
-    });
-
-    it('can be applied to arrays', function() {
-      eq(S.xor(empty, empty2).length, 0);
-      eq(S.xor(empty, nonempty), nonempty);
-      eq(S.xor(nonempty, empty), nonempty);
-      eq(S.xor(nonempty, nonempty2).length, 0);
-    });
-
-    it('can be applied to maybes', function() {
-      eq(S.xor(nothing, nothing2).constructor, S.Nothing);
-      eq(S.xor(nothing, just), just);
-      eq(S.xor(just, nothing), just);
-      eq(S.xor(just, just2).constructor, S.Nothing);
-    });
-
-    it('throws if applied to values of different types', function() {
-      function Foo() {}
-      Foo.prototype.type = Foo;
-      var foo = new Foo();
-
-      assert.throws(
-        function() { S.xor(empty, nothing); },
-        function(err) {
-          return err instanceof TypeError &&
-                 err.message === 'Type mismatch';
-        }
-      );
-
+    it('throws if applied to non-functors', function() {
       assert.throws(
-        function() { S.xor(nothing, foo); },
+        function() { S.or(42, 42); },
         function(err) {
           return err instanceof TypeError &&
-                 err.message === 'Type mismatch';
+                 err.message === '"or" unspecified for Number';
         }
       );
     });
 
     it('is curried', function() {
-      eq(S.xor(empty)(nonempty), nonempty);
+      eq(S.or(empty)(nonempty), nonempty);
     });
 
   });