Repository: plaid/sanctuary
Pull request #26 (2015-05-07 23:32:11)

control: add S.and and S.xor
> `S.and`, `S.or`, and `S.xor` are defined in terms of a private `toBoolean` function which dispatches to each operand's `toBoolean` method if available. `Just.prototype.or` and `Nothing.prototype.or` are no longer necessary so are removed in this commit.

I don't know whether `S.xor` will be useful in practice, but I know how to find out. :)



Patch:
diff --git a/index.js b/index.js
index 90911db..ea00f78 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) {
@@ -233,16 +233,41 @@
 
   //  control  ///////////////////////////////////////////////////////////////
 
-  //  or :: f a -> f a -> f a
-  S.or = R.curry(function(x, y) {
+  var assertTypeMatch = function(x, y) {
     if (R.type(x) !== R.type(y) || x.type !== y.type) {
       throw new TypeError('Type mismatch');
-    } 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);
+    }
+  };
+
+  //  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();
     }
   });
 


Test patch:
diff --git a/test/index.js b/test/index.js
index 3284452..4097c25 100644
--- a/test/index.js
+++ b/test/index.js
@@ -150,12 +150,10 @@ describe('maybe', function() {
       eq(nothing.map(function() { return 42; }), nothing);
     });
 
-    it('provides an "or" method', function() {
+    it('provides a "toBoolean" method', function() {
       var nothing = S.Nothing();
-      var just = S.Just(42);
-      eq(S.Nothing().or.length, 1);
-      eq(S.Nothing().or(nothing), nothing);
-      eq(S.Nothing().or(just), just);
+      eq(nothing.toBoolean.length, 0);
+      eq(nothing.toBoolean(), false);
     });
 
     it('implements Semigroup', function() {
@@ -320,11 +318,10 @@ describe('maybe', function() {
       assert(just.map(function(x) { return x / 2; }).equals(S.Just(21)));
     });
 
-    it('provides an "or" method', function() {
+    it('provides a "toBoolean" method', function() {
       var just = S.Just(42);
-      eq(just.or.length, 1);
-      eq(just.or(S.Nothing()), just);
-      eq(just.or(S.Just(42)), just);
+      eq(just.toBoolean.length, 0);
+      eq(just.toBoolean(), true);
     });
 
     it('implements Semigroup', function() {
@@ -640,23 +637,86 @@ describe('either', function() {
 
 describe('control', function() {
 
-  describe('or', 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);
+    });
 
-    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('or', function() {
 
     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);
@@ -693,18 +753,64 @@ describe('control', function() {
       );
     });
 
-    it('throws if applied to non-functors', 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.or(42, 42); },
+        function() { S.xor(empty, nothing); },
         function(err) {
           return err instanceof TypeError &&
-                 err.message === '"or" unspecified for Number';
+                 err.message === 'Type mismatch';
+        }
+      );
+
+      assert.throws(
+        function() { S.xor(nothing, foo); },
+        function(err) {
+          return err instanceof TypeError &&
+                 err.message === 'Type mismatch';
         }
       );
     });
 
     it('is curried', function() {
-      eq(S.or(empty)(nonempty), nonempty);
+      eq(S.xor(empty)(nonempty), nonempty);
     });
 
   });