Repository: plaid/sanctuary
Pull request #121 (2015-11-17 04:38:33)

Add traversable instance for Maybe
Implements #118 

As per the included docs:

**Maybe#sequence :: Applicative f => Maybe (f a) ~> (b -> f b) -> f (Maybe a)**

Evaluates an applicative action contained within the Maybe, resulting in:
  - an applicative of `Just` the value of the evaluated action if `this`
    is a `Just`.
  - a pure applicative of `Nothing` if `this` is a `Nothing`.
```javascript
> S.Nothing().sequence(S.Either.of)
Right(Nothing())

> S.Just(Right(42)).sequence(S.Either.of)
Right(Just(42))
```

davidchambers: That was quick, Scott! Thanks!

davidchambers: Lovely pull request, Scott. Now that I've appeased my pedantic side it's time to spend some time studying these laws to make sure I understand them. :)

scott-christopher: This loose translation from [here](https://hackage.haskell.org/package/base-4.8.1.0/docs/Data-Traversable.html#t:Traversable) _might_ help with interpreting the laws:

---

A definition of `sequence` must satisfy the following laws:

**naturality**
* `compose(t, sequence) = compose(sequence, map(t))` for every applicative transformation `t`, where an applicative transformation is a function: `t :: (Applicative f, Applicative g) => f a -> g a`

**identity**
* `compose(sequence, map(Identity)) = Identity`

**composition**
* `compose(sequence, map(Compose)) = compose(Compose, map(sequence), sequence)`
davidchambers: I'm sorry for neglecting this pull request, Scott! Would you mind rebasing?

scott-christopher: > I'm sorry for neglecting this pull request

I forgot about it myself :)

I've rebased on to master and also updated the tests for `sequence` to use jsverify.
davidchambers: >       1) maybe Maybe implements Traversable:
>          Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.

Looks as though we need a bit more :hourglass:.

scott-christopher: I've split them out into separate `it` blocks to give them each a little more time to run.

Patch:
diff --git a/index.js b/index.js
index e7fc123..f77f46c 100644
--- a/index.js
+++ b/index.js
@@ -238,7 +238,6 @@
 
   //  env :: [Type]
   var env = $.env.concat([
-    $.FiniteNumber,
     $Either,
     Integer,
     List,
@@ -541,8 +540,8 @@
   //. The Maybe type represents optional values: a value of type `Maybe a` is
   //. either a Just whose value is of type `a` or a Nothing (with no value).
   //.
-  //. The Maybe type satisfies the [Monoid][], [Monad][], [Foldable][], and
-  //. [Extend][] specifications.
+  //. The Maybe type satisfies the [Monoid][], [Monad][], [Traversable][],
+  //. and [Extend][] specifications.
   var Maybe = S.Maybe = function Maybe() {
     if (arguments[0] !== sentinel) {
       throw new Error('Cannot instantiate Maybe');
@@ -832,6 +831,29 @@
            return maybe.isJust ? f(x, maybe.value) : x;
          });
 
+  //# Maybe#sequence :: Applicative f => Maybe (f a) ~> (a -> f a) -> f (Maybe a)
+  //.
+  //. Evaluates an applicative action contained within the Maybe, resulting in:
+  //.
+  //.   - a pure applicative of a Nothing if `this` is a Nothing; otherwise
+  //.
+  //.   - an applicative of Just the value of the evaluated action.
+  //.
+  //. ```javascript
+  //. > S.Nothing().sequence(S.Either.of)
+  //. Right(Nothing())
+  //.
+  //. > S.Just(Right(42)).sequence(S.Either.of)
+  //. Right(Just(42))
+  //. ```
+  Maybe.prototype.sequence =
+  method('Maybe#sequence',
+         {a: [Apply], b: [Apply]},
+         [$Maybe(a), $.Function, b],
+         function(maybe, of) {
+           return maybe.isJust ? R.map(Just, maybe.value) : of(maybe);
+         });
+
   //# Maybe#toBoolean :: Maybe a ~> Boolean
   //.
   //. Returns `false` if `this` is a Nothing; `true` if `this` is a Just.
@@ -2196,36 +2218,6 @@
         return filter(is(type), Just(x));
       });
 
-  //. ### Number
-
-  //# add :: FiniteNumber -> FiniteNumber -> FiniteNumber
-  //.
-  //. Returns the sum of two (finite) numbers.
-  //.
-  //. ```javascript
-  //. > S.add(1, 1)
-  //. 2
-  //. ```
-  S.add =
-  def('add',
-      {},
-      [$.FiniteNumber, $.FiniteNumber, $.FiniteNumber],
-      function(a, b) { return a + b; });
-
-  //# sub :: FiniteNumber -> FiniteNumber -> FiniteNumber
-  //.
-  //. Returns the difference between two (finite) numbers.
-  //.
-  //. ```javascript
-  //. > S.sub(4, 2)
-  //. 2
-  //. ```
-  S.sub =
-  def('sub',
-      {},
-      [$.FiniteNumber, $.FiniteNumber, $.FiniteNumber],
-      function(a, b) { return a - b; });
-
   //. ### Parse
 
   //# parseDate :: String -> Maybe Date
@@ -2522,10 +2514,10 @@
 
 //. [Apply]:        https://github.com/fantasyland/fantasy-land#apply
 //. [Extend]:       https://github.com/fantasyland/fantasy-land#extend
-//. [Foldable]:     https://github.com/fantasyland/fantasy-land#foldable
 //. [Functor]:      https://github.com/fantasyland/fantasy-land#functor
 //. [Monad]:        https://github.com/fantasyland/fantasy-land#monad
 //. [Monoid]:       https://github.com/fantasyland/fantasy-land#monoid
+//. [Traversable]:  https://github.com/fantasyland/fantasy-land#traversable
 //. [R.equals]:     http://ramdajs.com/docs/#equals
 //. [R.map]:        http://ramdajs.com/docs/#map
 //. [R.type]:       http://ramdajs.com/docs/#type


Test patch:
diff --git a/test/index.js b/test/index.js
index 67d1f6d..704674a 100644
--- a/test/index.js
+++ b/test/index.js
@@ -67,6 +67,79 @@ var squareRoot = function(n) {
                : S.Right(Math.sqrt(n));
 };
 
+//  Identity :: a -> Identity a
+var Identity = function(x) {
+  return {
+    map: function(fn) {
+      return Identity(fn(x));
+    },
+    ap: function(y) {
+      return Identity(x(y));
+    },
+    equals: function(other) {
+      return R.equals(x, other.value);
+    },
+    value: x
+  };
+};
+Identity.of = Identity;
+
+//  Compose :: Apply f, Apply g
+//          => { of: b -> f b } -> { of: c -> g c }
+//          -> f (g a) -> Compose f g a
+var Compose = function(F, G) {
+  function _Compose(x) {
+    return {
+      map: function(f) {
+        return _Compose(R.map(R.map(f), x));
+      },
+      ap: function(y) {
+        return _Compose(R.ap(R.map(R.ap, x), y.value));
+      },
+      equals: function(other) {
+        return R.equals(x, other.value);
+      },
+      value: x
+    };
+  }
+  _Compose.of = function(x) {
+    return _Compose(F.of(G.of(x)));
+  };
+  return _Compose;
+};
+
+//  eitherToMaybe :: Either a b -> Maybe b
+var eitherToMaybe = S.either(S.K(S.Nothing()), S.Just);
+
+//  JustArb :: Arbitrary a -> Arbitrary (Maybe a)
+var JustArb = function(arb) {
+  return arb.smap(S.Just, function(m) { return m.value; }, R.toString);
+};
+
+//  MaybeArb :: Arbitrary a -> Arbitrary (Maybe a)
+var MaybeArb = function(arb) {
+  return jsc.oneof(JustArb(arb), jsc.constant(S.Nothing()));
+};
+
+//  LeftArb :: Arbitrary a -> Arbitrary (Either a b)
+var LeftArb = function(arb) {
+  return arb.smap(S.Left, function(e) { return e.value; }, R.toString);
+};
+
+//  RightArb :: Arbitrary a -> Arbitrary (Either b a)
+var RightArb = function(arb) {
+  return arb.smap(S.Right, function(e) { return e.value; }, R.toString);
+};
+
+//  EitherArb :: Arbitrary a -> Arbitrary b -> Arbitrary (Either a b)
+var EitherArb = function(lArb, rArb) {
+  return jsc.oneof(LeftArb(lArb), RightArb(rArb));
+};
+
+//  IdentityArb :: Arbitrary a -> Arbitrary (Identity a)
+var IdentityArb = function(arb) {
+  return arb.smap(Identity, function(i) { return i.value; });
+};
 
 describe('invariants', function() {
 
@@ -478,6 +551,37 @@ describe('maybe', function() {
                     errorEq(Error, 'Cannot instantiate Maybe'));
     });
 
+    describe('Traversable laws', function() {
+
+      it('satisfies naturality', function() {
+        jsc.assert(jsc.forall(MaybeArb(EitherArb(jsc.integer, jsc.string)), function(maybe) {
+          var lhs = eitherToMaybe(maybe.sequence(S.Either.of));
+          var rhs = maybe.map(eitherToMaybe).sequence(S.Maybe.of);
+          return lhs.equals(rhs);
+        }));
+      });
+
+      it('satisfies identity', function() {
+        jsc.assert(jsc.forall(MaybeArb(jsc.integer), function(maybe) {
+          var lhs = maybe.map(Identity).sequence(Identity.of);
+          var rhs = Identity.of(maybe);
+          return lhs.equals(rhs);
+        }));
+      });
+
+      it('satisfies composition', function() {
+        jsc.assert(jsc.forall(MaybeArb(IdentityArb(MaybeArb(jsc.integer))), function(u) {
+          var C = Compose(Identity, S.Maybe);
+          var lhs = u.map(C).sequence(C.of);
+          var rhs = C(u.sequence(Identity.of).map(function(x) {
+            return x.sequence(S.Maybe.of);
+          }));
+          return lhs.equals(rhs);
+        }));
+      });
+
+    });
+
   });
 
   describe('Nothing', function() {
@@ -525,8 +629,7 @@ describe('maybe', function() {
       assert.throws(function() { S.Nothing().concat(S.Just(1)); },
                     errorEq(TypeError,
                             '‘Maybe#concat’ requires ‘a’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
     });
 
     it('provides an "equals" method', function() {
@@ -594,6 +697,11 @@ describe('maybe', function() {
                             'Function as its first argument; received null'));
     });
 
+    it('provides a "sequence" method', function() {
+      eq(S.Nothing().sequence.length, 1);
+      eq(S.Nothing().sequence(S.Either.of), S.Right(S.Nothing()));
+    });
+
     it('provides a "toBoolean" method', function() {
       eq(S.Nothing().toBoolean.length, 0);
       eq(S.Nothing().toBoolean(), false);
@@ -736,20 +844,17 @@ describe('maybe', function() {
       assert.throws(function() { S.Just(1).concat(S.Just(0)); },
                     errorEq(TypeError,
                             '‘Maybe#concat’ requires ‘a’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
 
       assert.throws(function() { S.Just(2).concat(S.Just([1, 2, 3])); },
                     errorEq(TypeError,
                             '‘Maybe#concat’ requires ‘a’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
 
       assert.throws(function() { S.Just([1, 2, 3]).concat(S.Just(3)); },
                     errorEq(TypeError,
                             '‘Maybe#concat’ requires ‘a’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
     });
 
     it('provides an "equals" method', function() {
@@ -828,6 +933,11 @@ describe('maybe', function() {
                             'Function as its first argument; received null'));
     });
 
+    it('provides a "sequence" method', function() {
+      eq(S.Just(S.Right(42)).sequence.length, 1);
+      eq(S.Just(S.Right(42)).sequence(S.Either.of), S.Right(S.Just(42)));
+    });
+
     it('provides a "toBoolean" method', function() {
       eq(S.Just(42).toBoolean.length, 0);
       eq(S.Just(42).toBoolean(), true);
@@ -983,9 +1093,8 @@ describe('maybe', function() {
       assert.throws(function() { S.fromMaybe(0, [1, 2, 3]); },
                     errorEq(TypeError,
                             '‘fromMaybe’ expected a value of type ' +
-                            '(Maybe Number) or (Maybe FiniteNumber) or ' +
-                            '(Maybe Integer) as its second argument; ' +
-                            'received [1, 2, 3]'));
+                            '(Maybe Number) or (Maybe Integer) ' +
+                            'as its second argument; received [1, 2, 3]'));
     });
 
     it('can be applied to a Nothing', function() {
@@ -1277,38 +1386,32 @@ describe('either', function() {
       assert.throws(function() { S.Left(1).concat(S.Left(0)); },
                     errorEq(TypeError,
                             '‘Either#concat’ requires ‘a’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
 
       assert.throws(function() { S.Left(2).concat(S.Right(0)); },
                     errorEq(TypeError,
                             '‘Either#concat’ requires ‘b’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
 
       assert.throws(function() { S.Left(3).concat(S.Left([1, 2, 3])); },
                     errorEq(TypeError,
                             '‘Either#concat’ requires ‘a’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
 
       assert.throws(function() { S.Left(4).concat(S.Right([1, 2, 3])); },
                     errorEq(TypeError,
                             '‘Either#concat’ requires ‘a’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
 
       assert.throws(function() { S.Left([1, 2, 3]).concat(S.Left(5)); },
                     errorEq(TypeError,
                             '‘Either#concat’ requires ‘a’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
 
       assert.throws(function() { S.Left([1, 2, 3]).concat(S.Right(6)); },
                     errorEq(TypeError,
                             '‘Either#concat’ requires ‘b’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
     });
 
     it('provides an "equals" method', function() {
@@ -1486,38 +1589,32 @@ describe('either', function() {
       assert.throws(function() { S.Right(1).concat(S.Left(0)); },
                     errorEq(TypeError,
                             '‘Either#concat’ requires ‘a’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
 
       assert.throws(function() { S.Right(2).concat(S.Right(0)); },
                     errorEq(TypeError,
                             '‘Either#concat’ requires ‘b’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
 
       assert.throws(function() { S.Right(3).concat(S.Left([1, 2, 3])); },
                     errorEq(TypeError,
                             '‘Either#concat’ requires ‘b’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
 
       assert.throws(function() { S.Right(4).concat(S.Right([1, 2, 3])); },
                     errorEq(TypeError,
                             '‘Either#concat’ requires ‘b’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
 
       assert.throws(function() { S.Right([1, 2, 3]).concat(S.Left(5)); },
                     errorEq(TypeError,
                             '‘Either#concat’ requires ‘a’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
 
       assert.throws(function() { S.Right([1, 2, 3]).concat(S.Right(6)); },
                     errorEq(TypeError,
                             '‘Either#concat’ requires ‘b’ to implement ' +
-                            'Semigroup; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'Semigroup; Number and Integer do not'));
     });
 
     it('provides an "equals" method', function() {
@@ -2008,7 +2105,7 @@ describe('alternative', function() {
       assert.throws(function() { S.and(0, 1); },
                     errorEq(TypeError,
                             '‘and’ requires ‘a’ to implement Alternative; ' +
-                            'Number and FiniteNumber and Integer do not'));
+                            'Number and Integer do not'));
     });
 
     it('is curried', function() {
@@ -2070,7 +2167,7 @@ describe('alternative', function() {
       assert.throws(function() { S.or(0, 1); },
                     errorEq(TypeError,
                             '‘or’ requires ‘a’ to implement Alternative; ' +
-                            'Number and FiniteNumber and Integer do not'));
+                            'Number and Integer do not'));
     });
 
     it('is curried', function() {
@@ -2125,14 +2222,12 @@ describe('alternative', function() {
                     errorEq(TypeError,
                             '‘xor’ requires ‘a’ to implement Alternative ' +
                             'and Monoid; (Either ??? Number) and ' +
-                            '(Either ??? FiniteNumber) and ' +
                             '(Either ??? Integer) do not'));
 
       assert.throws(function() { S.xor(S.Right(42), S.Right(43)); },
                     errorEq(TypeError,
                             '‘xor’ requires ‘a’ to implement Alternative ' +
                             'and Monoid; (Either ??? Number) and ' +
-                            '(Either ??? FiniteNumber) and ' +
                             '(Either ??? Integer) do not'));
     });
 
@@ -2153,8 +2248,7 @@ describe('alternative', function() {
       assert.throws(function() { S.xor(0, 1); },
                     errorEq(TypeError,
                             '‘xor’ requires ‘a’ to implement Alternative ' +
-                            'and Monoid; Number and FiniteNumber and Integer ' +
-                            'do not'));
+                            'and Monoid; Number and Integer do not'));
     });
 
     it('is curried', function() {
@@ -3106,96 +3200,6 @@ describe('object', function() {
 
 });
 
-describe('number', function() {
-
-  describe('add', function() {
-
-    it('is a binary function', function() {
-      eq(typeof S.add, 'function');
-      eq(S.add.length, 2);
-    });
-
-    it('type checks its arguments', function() {
-      assert.throws(function() { S.add('xxx', 1); },
-                    errorEq(TypeError,
-                            '‘add’ expected a value of type FiniteNumber ' +
-                            'as its first argument; received "xxx"'));
-
-      assert.throws(function() { S.add(1, 'xxx'); },
-                    errorEq(TypeError,
-                            '‘add’ expected a value of type FiniteNumber ' +
-                            'as its second argument; received "xxx"'));
-
-      assert.throws(function() { S.add(1, Infinity); },
-                    errorEq(TypeError,
-                            '‘add’ expected a value of type FiniteNumber ' +
-                            'as its second argument; received Infinity'));
-
-      assert.throws(function() { S.add(1, -Infinity); },
-                    errorEq(TypeError,
-                            '‘add’ expected a value of type FiniteNumber ' +
-                            'as its second argument; received -Infinity'));
-    });
-
-    it('adds two numbers', function() {
-      eq(S.add(1, 1), 2);
-      eq(S.add(-1, -1), -2);
-      eq(S.add(1.5, 1), 2.5);
-      eq(S.add(-1.5, -1), -2.5);
-    });
-
-    it('is curried', function() {
-      eq(S.add(1).length, 1);
-      eq(S.add(1)(1), 2);
-    });
-
-  });
-
-  describe('sub', function() {
-
-    it('is a binary function', function() {
-      eq(typeof S.sub, 'function');
-      eq(S.sub.length, 2);
-    });
-
-    it('type checks its arguments', function() {
-      assert.throws(function() { S.sub('xxx', 1); },
-                    errorEq(TypeError,
-                            '‘sub’ expected a value of type FiniteNumber ' +
-                            'as its first argument; received "xxx"'));
-
-      assert.throws(function() { S.sub(1, 'xxx'); },
-                    errorEq(TypeError,
-                            '‘sub’ expected a value of type FiniteNumber ' +
-                            'as its second argument; received "xxx"'));
-
-      assert.throws(function() { S.sub(1, Infinity); },
-                    errorEq(TypeError,
-                            '‘sub’ expected a value of type FiniteNumber ' +
-                            'as its second argument; received Infinity'));
-
-      assert.throws(function() { S.sub(1, -Infinity); },
-                    errorEq(TypeError,
-                            '‘sub’ expected a value of type FiniteNumber ' +
-                            'as its second argument; received -Infinity'));
-    });
-
-    it('subtracts two numbers', function() {
-      eq(S.sub(1, 1), 0);
-      eq(S.sub(-1, -1), 0);
-      eq(S.sub(7.5, 2), 5.5);
-      eq(S.sub(-7.5, -2), -5.5);
-    });
-
-    it('is curried', function() {
-      eq(S.sub(1).length, 1);
-      eq(S.sub(1)(1), 0);
-    });
-
-  });
-
-});
-
 describe('parse', function() {
 
   describe('parseDate', function() {