Repository: plaid/sanctuary
Pull request #8 (2015-02-09 06:35:02)

maybe: implement Apply, Applicative, Chain, and Monad
Reading fantasyland/fantasy-land#85 improved my understanding of these types, and motivated me to add the methods necessary for Maybe to implement the Apply, Applicative, Chain, and Monad specifications.


davidchambers: I haven't yet merged this as I'm hoping that fantasyland/fantasy-land#90 will be accepted, at which point I'll update the identifiers in the tests to match the updated specification.


Patch:
diff --git a/index.js b/index.js
index 225fbfa..6ca96a9 100644
--- a/index.js
+++ b/index.js
@@ -46,6 +46,13 @@
     throw new Error('Cannot instantiate Maybe');
   }
 
+  //  Maybe.of :: a -> m a
+  Maybe.of = function(x) {
+    return new Just(x);
+  };
+
+  Maybe.prototype.of = Maybe.of;
+
   Maybe.prototype.type = Maybe;
 
   function Nothing() {
@@ -55,6 +62,11 @@
   }
   extend(Nothing, Maybe);
 
+  //  Nothing#ap :: m a -> m b
+  Nothing.prototype.ap = function(x) {  // jshint ignore:line
+    return this;
+  };
+
   //  Nothing#chain :: (a -> m b) -> m b
   Nothing.prototype.chain = function(f) {  // jshint ignore:line
     return this;
@@ -84,6 +96,11 @@
   }
   extend(Just, Maybe);
 
+  //  Just#ap :: m a -> m b
+  Just.prototype.ap = function(x) {
+    return x.map(this.value);
+  };
+
   //  Just#chain :: (a -> m b) -> m b
   Just.prototype.chain = function(f) {
     return f(this.value);


Test patch:
diff --git a/test/index.js b/test/index.js
index da79f9f..cf45aca 100644
--- a/test/index.js
+++ b/test/index.js
@@ -9,6 +9,18 @@ var S = require('..');
 
 var eq = assert.strictEqual;
 
+//  identity :: a -> a
+var identity = function(x) { return x; };
+
+//  inc :: Number -> Number
+var inc = function(n) { return n + 1; };
+
+//  length :: [a] -> Number
+var length = function(xs) { return xs.length; };
+
+//  square :: Number -> Number
+var square = function(n) { return n * n; };
+
 
 describe('maybe', function() {
 
@@ -46,6 +58,13 @@ describe('maybe', function() {
       eq(nothing.type, S.Maybe);
     });
 
+    it('provides an "ap" method', function() {
+      var nothing = S.Nothing();
+      eq(nothing.ap.length, 1);
+      assert(nothing.ap(S.Nothing()).equals(S.Nothing()));
+      assert(nothing.ap(S.Just(42)).equals(S.Nothing()));
+    });
+
     it('provides a "chain" method', function() {
       var nothing = S.Nothing();
       eq(nothing.chain.length, 1);
@@ -75,6 +94,71 @@ describe('maybe', function() {
       eq(S.Nothing().or(just), just);
     });
 
+    it('implements Functor', function() {
+      var a = S.Nothing();
+      var f = inc;
+      var g = square;
+
+      // identity
+      assert(a.map(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.Nothing();
+      var b = S.Nothing();
+      var c = S.Nothing();
+
+      // 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.Nothing();
+      var b = S.Nothing();
+      var f = inc;
+      var x = 7;
+
+      // identity
+      assert(a.of(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.Nothing();
+      var f = S.head;
+      var g = S.last;
+
+      // associativity
+      assert(a.chain(f).chain(g)
+             .equals(a.chain(function(x) { return f(x).chain(g); })));
+    });
+
+    it('implements Monad', function() {
+      var a = S.Nothing();
+      var f = S.head;
+      var x = [1, 2, 3];
+
+      // left identity
+      assert(a.of(x).chain(f).equals(f(x)));
+
+      // right identity
+      assert(a.chain(a.of).equals(a));
+    });
+
   });
 
   describe('Just', function() {
@@ -98,6 +182,13 @@ describe('maybe', function() {
       eq(just.type, S.Maybe);
     });
 
+    it('provides an "ap" method', function() {
+      var just = S.Just(inc);
+      eq(just.ap.length, 1);
+      assert(just.ap(S.Nothing()).equals(S.Nothing()));
+      assert(just.ap(S.Just(42)).equals(S.Just(43)));
+    });
+
     it('provides a "chain" method', function() {
       var just = S.Just([1, 2, 3]);
       eq(just.chain.length, 1);
@@ -127,6 +218,71 @@ describe('maybe', function() {
       eq(just.or(S.Just(42)), just);
     });
 
+    it('implements Functor', function() {
+      var a = S.Just(7);
+      var f = inc;
+      var g = square;
+
+      // identity
+      assert(a.map(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.Just(inc);
+      var b = S.Just(square);
+      var c = S.Just(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.Just(null);
+      var b = S.Just(inc);
+      var f = inc;
+      var x = 7;
+
+      // identity
+      assert(a.of(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.Just([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);
+      var f = S.head;
+      var g = S.last;
+
+      // associativity
+      assert(a.chain(f).chain(g)
+             .equals(a.chain(function(x) { return f(x).chain(g); })));
+    });
+
+    it('implements Monad', function() {
+      var a = S.Just(null);
+      var f = S.head;
+      var x = [1, 2, 3];
+
+      // left identity
+      assert(a.of(x).chain(f).equals(f(x)));
+
+      // right identity
+      assert(a.chain(a.of).equals(a));
+    });
+
   });
 
   describe('fromMaybe', function() {
@@ -207,12 +363,6 @@ describe('maybe', function() {
 
 describe('either', function() {
 
-  //  length :: String -> Number
-  var length = function(s) { return s.length; };
-
-  //  square :: Number -> Number
-  var square = function(n) { return n * n; };
-
   describe('Either', function() {
 
     it('throws if called', function() {