From 7e3dca14cf2472376cfb9f3ed481fe27202d59ec Mon Sep 17 00:00:00 2001 From: David Chambers Date: Sun, 8 Feb 2015 22:24:49 -0800 Subject: [PATCH] maybe: implement Apply, Applicative, Chain, and Monad --- index.js | 17 ++++++ test/index.js | 162 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 173 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 225fbfa7..6ca96a96 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); diff --git a/test/index.js b/test/index.js index da79f9fe..cf45aca4 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() {