Repository: plaid/sanctuary
Pull request #33 (2015-05-14 23:42:38)

either: define "toBoolean"
This allows `S.and` and `S.or` to be applied to values of the Either type:

```javascript
> S.and(S.Left('abc'), S.Left('xyz'))
Left('abc')
> S.and(S.Left('abc'), S.Right(1))
Left('abc')
> S.and(S.Right(1), S.Left('abc'))
Left('abc')
> S.and(S.Right(1), S.Right(2))
Right(2)
> S.or(S.Left('abc'), S.Left('xyz'))
Left('xyz')
> S.or(S.Left('abc'), S.Right(1))
Right(1)
> S.or(S.Right(1), S.Left('abc'))
Right(1)
> S.or(S.Right(1), S.Right(2))
Right(1)
```

`S.xor` cannot be applied to values of the Either type since the type lacks a canonical empty value:

```javascript
> S.xor(S.Left('abc'), S.Left('xyz'))
TypeError: Left("abc") does not have an "empty" method
```

For the sake of consistency, an error is thrown even when the empty value is not needed:

```javascript
> S.xor(S.Left('abc'), S.Right(1))
TypeError: Left("abc") does not have an "empty" method
```

This pull request also adds nice error reporting for the case where the arguments to `S.and`, `S.or`, or `S.xor` lack a `toBoolean` method:

```javascript
> S.and({x: 1, y: 1}, {x: 2, y: 2})
TypeError: {"x": 1, "y": 1} does not have a "toBoolean" method
```

@michaelckelly, this pull request may be of particular interest to you in light of our discussion yesterday about how to take one Either *or* another.



Patch:
diff --git a/.jscsrc b/.jscsrc
index 7ca1d28..178eedb 100644
--- a/.jscsrc
+++ b/.jscsrc
@@ -17,7 +17,7 @@
   "maximumLineLength": {"value": 79, "allowRegex": true},
   "requireCapitalizedConstructors": true,
   "requireCommaBeforeLineBreak": true,
-  "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch", "finally"],
+  "requireCurlyBraces": ["else", "for", "while", "do", "try", "catch", "finally"],
   "requireDotNotation": true,
   "requireLineFeedAtFileEnd": true,
   "requireParenthesesAroundIIFE": true,
diff --git a/.jshintrc b/.jshintrc
index 447ce51..3e43642 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -1,7 +1,7 @@
 {
   "bitwise": false,
   "camelcase": false,
-  "curly": true,
+  "curly": false,
   "eqeqeq": true,
   "forin": false,
   "freeze": false,
diff --git a/index.js b/index.js
index 52426ae..97ab1d8 100644
--- a/index.js
+++ b/index.js
@@ -233,6 +233,11 @@
     return this;
   };
 
+  //  Left#toBoolean :: -> Boolean
+  Left.prototype.toBoolean = function() {
+    return false;
+  };
+
   //  Left#toString :: -> String
   Left.prototype.toString = function() {
     return 'Left(' + R.toString(this.value) + ')';
@@ -271,6 +276,11 @@
     return new Right(f(this.value));
   };
 
+  //  Right#toBoolean :: -> Boolean
+  Right.prototype.toBoolean = function() {
+    return true;
+  };
+
   //  Right#toString :: -> String
   Right.prototype.toString = function() {
     return 'Right(' + R.toString(this.value) + ')';
@@ -298,11 +308,18 @@
 
   //  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();
-    }
+    if (R.is(Array, x))               return x.length > 0;
+    if (R.is(Boolean, x))             return x;
+    if (R.is(Function, x.toBoolean))  return x.toBoolean();
+    throw new TypeError(R.toString(x) + ' does not have a "toBoolean" method');
+  };
+
+  //  empty :: a -> a
+  var empty = function(x) {
+    if (R.is(Array, x))               return [];
+    if (R.is(Boolean, x))             return false;
+    if (R.is(Function, x.empty))      return x.empty();
+    throw new TypeError(R.toString(x) + ' does not have an "empty" method');
   };
 
   //  and :: a -> a -> a
@@ -320,12 +337,10 @@
   //  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();
-    }
+    var xBool = toBoolean(x);
+    var yBool = toBoolean(y);
+    var xEmpty = empty(x);
+    return xBool !== yBool ? or(x, y) : xEmpty;
   });
 
   //  list  //////////////////////////////////////////////////////////////////


Test patch:
diff --git a/test/index.js b/test/index.js
index 9aa51d2..c4c9313 100644
--- a/test/index.js
+++ b/test/index.js
@@ -601,6 +601,12 @@ describe('either', function() {
       eq(left.map(square), left);
     });
 
+    it('provides a "toBoolean" method', function() {
+      var left = S.Left('Cannot divide by zero');
+      eq(left.toBoolean.length, 0);
+      eq(left.toBoolean(), false);
+    });
+
     it('provides a "toString" method', function() {
       var left = S.Left('Cannot divide by zero');
       eq(left.toString.length, 0);
@@ -740,6 +746,12 @@ describe('either', function() {
       assert(right.map(square).equals(S.Right(1764)));
     });
 
+    it('provides a "toBoolean" method', function() {
+      var right = S.Right(42);
+      eq(right.toBoolean.length, 0);
+      eq(right.toBoolean(), true);
+    });
+
     it('provides a "toString" method', function() {
       var right = S.Right([1, 2, 3]);
       eq(right.toString.length, 0);
@@ -864,6 +876,11 @@ describe('control', function() {
   var just = S.Just(42);
   var just2 = S.Just(42);
 
+  var left = S.Left('Invalid JSON');
+  var left2 = S.Left('Cannot divide by zero');
+  var right = S.Right(42);
+  var right2 = S.Right(42);
+
   describe('and', function() {
 
     it('is a binary function', function() {
@@ -892,6 +909,13 @@ describe('control', function() {
       eq(S.and(just, just2), just2);
     });
 
+    it('can be applied to eithers', function() {
+      eq(S.and(left, left2), left);
+      eq(S.and(left, right), left);
+      eq(S.and(right, left), left);
+      eq(S.and(right, right2), right2);
+    });
+
     it('throws if applied to values of different types', function() {
       function Foo() {}
       Foo.prototype.type = Foo;
@@ -904,6 +928,12 @@ describe('control', function() {
                     isTypeMismatch);
     });
 
+    it('throws if applied to values without a "toBoolean" method', function() {
+      assert.throws(function() { S.and(0, 1); },
+                    R.both(R.is(TypeError),
+                           messageEq('0 does not have a "toBoolean" method')));
+    });
+
     it('is curried', function() {
       eq(S.and(empty)(nonempty), empty);
     });
@@ -938,6 +968,13 @@ describe('control', function() {
       eq(S.or(just, just2), just);
     });
 
+    it('can be applied to eithers', function() {
+      eq(S.or(left, left2), left2);
+      eq(S.or(left, right), right);
+      eq(S.or(right, left), right);
+      eq(S.or(right, right2), right);
+    });
+
     it('throws if applied to values of different types', function() {
       function Foo() {}
       Foo.prototype.type = Foo;
@@ -950,6 +987,12 @@ describe('control', function() {
                     isTypeMismatch);
     });
 
+    it('throws if applied to values without a "toBoolean" method', function() {
+      assert.throws(function() { S.or(0, 1); },
+                    R.both(R.is(TypeError),
+                           messageEq('0 does not have a "toBoolean" method')));
+    });
+
     it('is curried', function() {
       eq(S.or(empty)(nonempty), nonempty);
     });
@@ -984,6 +1027,27 @@ describe('control', function() {
       eq(S.xor(just, just2).constructor, S.Nothing);
     });
 
+    it('cannot be applied to eithers', function() {
+      //  message :: String -> String
+      var message = R.concat(R.__, ' does not have an "empty" method');
+
+      assert.throws(function() { S.xor(left, left2); },
+                    R.both(R.is(TypeError),
+                           messageEq(message('Left("Invalid JSON")'))));
+
+      assert.throws(function() { S.xor(left, right); },
+                    R.both(R.is(TypeError),
+                           messageEq(message('Left("Invalid JSON")'))));
+
+      assert.throws(function() { S.xor(right, left); },
+                    R.both(R.is(TypeError),
+                           messageEq(message('Right(42)'))));
+
+      assert.throws(function() { S.xor(right, right2); },
+                    R.both(R.is(TypeError),
+                           messageEq(message('Right(42)'))));
+    });
+
     it('throws if applied to values of different types', function() {
       function Foo() {}
       Foo.prototype.type = Foo;
@@ -996,6 +1060,12 @@ describe('control', function() {
                     isTypeMismatch);
     });
 
+    it('throws if applied to values without a "toBoolean" method', function() {
+      assert.throws(function() { S.xor(0, 1); },
+                    R.both(R.is(TypeError),
+                           messageEq('0 does not have a "toBoolean" method')));
+    });
+
     it('is curried', function() {
       eq(S.xor(empty)(nonempty), nonempty);
     });