From 7fcc9407574f3e98569e372e05b0f0b53de8f740 Mon Sep 17 00:00:00 2001 From: Irakli Safareli Date: Thu, 8 Sep 2016 19:18:59 +0400 Subject: [PATCH 01/11] Make Future.equals return promise If feature is not resolved synchronously then old implementation produces strange fails. this way we could express equality of async Features. Also use deepEqual instead of equal. --- package.json | 1 + test/future.test.js | 90 +++++++++++++++++++++++++++++++-------------- 2 files changed, 63 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 0d6bf75..6562d30 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "jshint": "~2.7.0", "jsverify": "^0.7.1", "mocha": "^2.1.0", + "promise": "7.1.1", "uglify-js": "2.4.x", "xyz": "0.5.x" } diff --git a/test/future.test.js b/test/future.test.js index 2962227..470a0ae 100644 --- a/test/future.test.js +++ b/test/future.test.js @@ -3,38 +3,71 @@ var assert = require('assert'); var equalsInvoker = require('./utils').equalsInvoker; var types = require('./types')(equalsInvoker); var Future = require('../src/Future'); +var Promise = require('promise'); Future.prototype.equals = function(b) { - this.fork(function(e1) { - b.fork(function(e2) { - assert.equal(e1, e2); - }, function() { - assert.fail(null, e1, 'Futures not equal: f1 failed, f2 did not', '==='); - }); - }, function(v1) { - b.fork(function() { - assert.fail(null, v1, 'Futures not equal: f1 succeeded, f2 did not', '==='); - }, function(v2) { - assert.equal(v1, v2); + var self = this; + return new Promise(function(resolve, reject) { + self.fork(function(e1) { + b.fork(function(e2) { + try { + assert.deepEqual(e1, e2); + } catch (e) { reject(e); } + resolve(); + }, function() { + try{ + assert.fail(null, e1, 'Futures not equal: f1 failed, f2 did not', '==='); + } catch (e) { reject(e); } + reject(); + }); + }, function(v1) { + b.fork(function() { + try{ + assert.fail(null, v1, 'Futures not equal: f1 succeeded, f2 did not', '==='); + } catch (e) { reject(e); } + reject(); + }, function(v2) { + try { + assert.deepEqual(v1, v2); + } catch (e) { reject(e); } + resolve(); + }); }); }); - return true; }; describe('Future', function() { - it('should equal another future', function() { - var f1 = Future.of(2); - var f2 = Future.of(2); - assert.equal(true, f1.equals(f2)); + describe('Equal', function() { + it('should equal another future', function() { + var f1 = Future.of(2); + var f2 = Future.of(2); + return f1.equals(f2); + }); + + it('should equal another future (async)', function() { + var f1 = Future.of(2); + var f2 = Future(function(rej, res) { + setTimeout(res, 1, 2); + }); + return f1.equals(f2); + }); + + it('should equal another future (non-primitive value)', function() { + var f1 = Future.of([2,2]); + var f2 = Future.of([2,2]); + return f1.equals(f2); + }); }); it('is a Functor', function() { var fTest = types.functor; var f = Future.of(2); assert.equal(true, fTest.iface(f)); - assert.equal(true, fTest.id(f)); - assert.equal(true, fTest.compose(f, R.multiply(2), R.add(3))); + return Promise.all([ + fTest.id(f), + fTest.compose(f, R.multiply(2), R.add(3)) + ]); }); it('is an Apply', function() { @@ -43,7 +76,7 @@ describe('Future', function() { var appU = Future.of(R.add(5)); var appV = Future.of(10); assert.equal(true, aTest.iface(appA)); - assert.equal(true, aTest.compose(appA, appU, appV)); + return aTest.compose(appA, appU, appV); }); it('is an Applicative', function() { @@ -53,9 +86,11 @@ describe('Future', function() { var appF = Future.of(R.multiply(3)); assert.equal(true, aTest.iface(app1)); - assert.equal(true, aTest.id(app1, app2)); - assert.equal(true, aTest.homomorphic(app1, R.add(3), 46)); - assert.equal(true, aTest.interchange(app1, appF, 17)); + return Promise.all([ + aTest.id(app1, app2), + aTest.homomorphic(app1, R.add(3), 46), + aTest.interchange(app1, appF, 17), + ]); }); it('is a Chain', function() { @@ -65,7 +100,7 @@ describe('Future', function() { var f2 = function(x) {return Future.of((5 + x));}; assert.equal(true, cTest.iface(f)); - assert.equal(true, cTest.associative(f, f1, f2)); + return cTest.associative(f, f1, f2); }); it('is a Monad', function() { @@ -76,7 +111,7 @@ describe('Future', function() { it('.map should work according to the functor specification', function() { var result = Future.of(1).map(R.inc); - assert.equal(true, Future.of(2).equals(result)); + return Future.of(2).equals(result); }); it('.chain should work according to the chainable specification', function() { @@ -84,14 +119,14 @@ describe('Future', function() { return Future.of(R.inc(val)); }; var result = Future.of(1).chain(incInTheFuture); - assert.equal(true, Future.of(2).equals(result)); + return Future.of(2).equals(result); }); describe('chainReject', function() { it('.chainReject should work like chain but off reject case', function() { var f1 = Future.reject(2); var f2 = function(val){ return Future.of(val + 3);}; - assert.equal(true, Future.of(5).equals(f1.chainReject(f2))); + return Future.of(5).equals(f1.chainReject(f2)); }); }); @@ -121,7 +156,7 @@ describe('Future', function() { it('applies its function to the passed in future', function() { var f1 = Future.of(add(1)); var result = f1.ap(Future.of(2)); - assert.equal(true, Future.of(3).equals(result)); + return Future.of(3).equals(result); }); it('does the apply in parallel', function(done) { @@ -396,4 +431,3 @@ describe('Future', function() { }); }); - From 7b285f2682aa9e261b3316ef637f80fcbb980212 Mon Sep 17 00:00:00 2001 From: Irakli Safareli Date: Thu, 8 Sep 2016 17:53:34 +0400 Subject: [PATCH 02/11] add `ChainRec` type to README and test/types.js --- README.md | 19 ++++++++++--------- test/types.js | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 70a31d1..b14b4d3 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,15 @@ not undergone thorough testing/use yet. ## Available types -| Name | [Setoid][3] | [Semigroup][4] | [Functor][5] | [Applicative][6] | [Monad][7] | [Foldable][8] | -| --------------- | :----------: | :------------: | :----------: | :--------------: | :--------: | :-----------: | -| [Either][9] | **✔︎** | | **✔︎** | **✔︎** | **✔︎** | | -| [Future][10] | | | **✔︎** | **✔︎** | **✔︎** | | -| [Identity][11] | **✔︎** | | **✔︎** | **✔︎** | **✔︎** | | -| [IO][12] | | | **✔︎** | **✔︎** | **✔︎** | | -| [Maybe][13] | **✔︎** | | **✔︎** | **✔︎** | **✔︎** | **✔︎** | -| [Reader][14] | | | **✔︎** | **✔︎** | **✔︎** | | -| [Tuple][15] | **✔︎** | **✔︎** | **✔︎** | | | | +| Name | [Setoid][3] | [Semigroup][4] | [Functor][5] | [Applicative][6] | [Monad][7] | [Foldable][8] | [ChainRec][16] | +| --------------- | :----------: | :------------: | :----------: | :--------------: | :--------: | :-----------: | :------------: | +| [Either][9] | **✔︎** | | **✔︎** | **✔︎** | **✔︎** | | **✔︎** | +| [Future][10] | | | **✔︎** | **✔︎** | **✔︎** | | **✔︎** | +| [Identity][11] | **✔︎** | | **✔︎** | **✔︎** | **✔︎** | | **✔︎** | +| [IO][12] | | | **✔︎** | **✔︎** | **✔︎** | | **✔︎** | +| [Maybe][13] | **✔︎** | | **✔︎** | **✔︎** | **✔︎** | **✔︎** | **✔︎** | +| [Reader][14] | | | **✔︎** | **✔︎** | **✔︎** | | **✔︎** | +| [Tuple][15] | **✔︎** | **✔︎** | **✔︎** | | | | | Access like so: @@ -41,3 +41,4 @@ Access like so: [13]: docs/Maybe.md [14]: docs/Reader.md [15]: docs/Tuple.md +[16]: https://github.com/fantasyland/fantasy-land#chainrec diff --git a/test/types.js b/test/types.js index c9b0f42..248a0aa 100644 --- a/test/types.js +++ b/test/types.js @@ -5,6 +5,7 @@ var interfaces = { apply: ['map', 'ap'], applicative: ['map', 'ap', 'of'], chain: ['map', 'ap', 'chain'], + chainRec: ['map', 'ap', 'chain', 'chainRec'], monad: ['map', 'ap', 'chain', 'of'], extend: ['extend'], comonad: ['extend', 'extract'], @@ -93,6 +94,19 @@ module.exports = function(eq) { ); } }, + chainRec: { + iface: correctInterface('chainRec'), + equivalence: function (T, p, d, n, x) { + return eq( + T.chainRec(function(next, done, v) { + return p(v) ? d(v).map(done) : n(v).map(next); + }, x), + (function step(v) { + return p(v) ? d(v) : n(v).chain(step); + }(x)) + ); + } + }, monad: { iface: correctInterface('monad') From 3e1020fb3cf697f669c83826abfdbb2ad451fd8b Mon Sep 17 00:00:00 2001 From: Irakli Safareli Date: Thu, 8 Sep 2016 17:54:37 +0400 Subject: [PATCH 03/11] implement ChainRec for Future --- src/Future.js | 83 +++++++++++++++++++++++++++++++++++++++++++++ test/future.test.js | 44 ++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/src/Future.js b/src/Future.js index e2794a9..35634aa 100644 --- a/src/Future.js +++ b/src/Future.js @@ -82,6 +82,89 @@ Future.prototype.chain = function(f) { // Sorella's: }.bind(this)); }; +var chainRecFork = function(t, rej, res) { + var isSync = false; + t.fork(function(v) { + var r = rej(v, !isSync); + isSync = true; + return r; + }, function(v) { + var r = res(v, !isSync); + isSync = true; + return r; + }); + if (isSync) { + return { isSync: true }; + } else { + isSync = true; + return { isSync: false }; + } +}; + +var chainRecNext = function(v) { + return function(onNext/*, onDone*/){ + return onNext(v); + }; +}; + +var chainRecDone = function(v) { + return function(onNext, onDone){ + return onDone(v); + }; +}; + +// chainRec +Future.chainRec = Future.prototype.chainRec = function(f, i) { + return new Future(function(reject, resolve) { + chainRecFork( + f(chainRecNext, chainRecDone, i), + function(z/*, isSync*/) { + return reject(z); + }, + function(fold, isSync) { + return fold( + function(v2) { + if (isSync === false) { + Future.chainRec(f, v2).fork(reject, resolve); + return; + } + var state = { loop: true, arg: v2 }; + var onReject = function(z/*, isSync*/) { + state.loop = false; + reject(z); + }; + var onResolve = function(fold, isSync) { + return fold( + function(v3) { + state = { loop: isSync, arg: v3 }; + if (isSync === false) { + Future.chainRec(f, v3).fork(reject, resolve); + } + }, + function(v) { + state = { loop: false}; + resolve(v); + } + ); + }; + while (state.loop) { + var forkRes = chainRecFork( + f(chainRecNext, chainRecDone, state.arg), + onReject, + onResolve + ); + if (forkRes.isSync === false) { + state = { loop: false}; + } + } + }, + resolve + ); + } + ); + }); +}; + // chainReject // Like chain but operates on the reject instead of the resolve case. //:: Future a, b => (a -> Future c) -> Future c diff --git a/test/future.test.js b/test/future.test.js index 470a0ae..151dcee 100644 --- a/test/future.test.js +++ b/test/future.test.js @@ -103,6 +103,50 @@ describe('Future', function() { return cTest.associative(f, f1, f2); }); + describe('ChainRec', function() { + it('is a ChainRec', function() { + var cTest = types.chainRec; + var predicate = function(a) { + return a.length > 5; + }; + var done = Future.of; + var x = 1; + var initial = [x]; + var next = function(a) { + return Future.of(a.concat([x])); + }; + assert.equal(true, cTest.iface(Future.of(1))); + return cTest.equivalence(Future, predicate, done, next, initial); + }); + + it('sync and async Futures', function() { + return Future.of('DONE').equals(Future.chainRec(function(next, done, n) { + if (n === 0) { + return Future.of(done('DONE')); + } else if (n > 100 || n === 1) { + return Future.of(next(n - 1)); + } else { + return new Future(function(rej, res) { setTimeout(res, 0, next(n - 1)); }); + } + }, 100000)); + }); + + it('fail Immediately', function() { + return Future.reject('ERROR').equals(Future.chainRec(function(/*next, done, n*/) { + return Future.reject('ERROR'); + }, 100)); + }); + + it('fail on next step', function() { + return Future.reject('ERROR').equals(Future.chainRec(function(next, done, n) { + if (n === 0) { + return Future.reject('ERROR'); + } + return Future.of(next(n - 1)); + }, 100)); + }); + }); + it('is a Monad', function() { var mTest = types.monad; var f = Future.of(null); From 9f90e0e96c12ac02aa42c74b40bde32f1db74be4 Mon Sep 17 00:00:00 2001 From: Irakli Safareli Date: Thu, 8 Sep 2016 19:43:45 +0400 Subject: [PATCH 04/11] implement ChainRec for Identity --- src/Identity.js | 17 +++++++++++++++++ test/identity.test.js | 27 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/Identity.js b/src/Identity.js index b8c9ab2..a18dc97 100644 --- a/src/Identity.js +++ b/src/Identity.js @@ -68,6 +68,23 @@ Identity.prototype.chain = function(fn) { return fn(this.value); }; +var chainRecNext = function(v) { + return { done: false, value: v}; +}; + +var chainRecDone = function(v) { + return { done: true, value: v}; +}; + +// chainRec +Identity.chainRec = Identity.prototype.chainRec = function(f, i) { + var state = { done:false, value: i}; + while(state.done === false) { + state = f(chainRecNext, chainRecDone, state.value).get(); + } + return Identity(state.value); +}; + /** * Returns the value of `Identity[a]` * diff --git a/test/identity.test.js b/test/identity.test.js index f722604..cd68450 100644 --- a/test/identity.test.js +++ b/test/identity.test.js @@ -57,6 +57,33 @@ describe('Identity', function() { assert.equal(true, cTest.associative(m, fNull, fNull)); }); + describe('ChainRec', function() { + it('is a ChainRec', function() { + var cTest = types.chainRec; + var predicate = function(a) { + return a.length > 5; + }; + var done = Identity.of; + var x = 1; + var initial = [x]; + var next = function(a) { + return Identity.of(a.concat([x])); + }; + assert.equal(true, cTest.iface(Identity.of(1))); + return cTest.equivalence(Identity, predicate, done, next, initial); + }); + + it('is stacksafe', function() { + return Identity.of('DONE').equals(Identity.chainRec(function(next, done, n) { + if (n === 0) { + return Identity.of(done('DONE')); + } else { + return Identity.of(next(n - 1)); + } + }, 100000)); + }); + }); + it('is a Monad', function() { var mTest = types.monad; assert.equal(true, mTest.iface(m)); From 15f77ac6cd008e14e80557ed3e0c6543e78099e7 Mon Sep 17 00:00:00 2001 From: Irakli Safareli Date: Thu, 8 Sep 2016 20:03:44 +0400 Subject: [PATCH 05/11] implement ChainRec for Maybe --- src/Maybe.js | 13 +++++++++++++ src/internal/util.js | 12 ++++++++++++ test/maybe.test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/src/Maybe.js b/src/Maybe.js index e204a4e..8a51d06 100644 --- a/src/Maybe.js +++ b/src/Maybe.js @@ -80,6 +80,19 @@ Just.prototype.chain = util.baseMap; Nothing.prototype.chain = util.returnThis; +//chainRec +Maybe.chainRec = Maybe.prototype.chainRec = function(f, i) { + var state = util.chainRecNext(i); + while (state.done === false) { + state = f(util.chainRecNext, util.chainRecDone, state.value).getOrElse({ done: true, isNothing:true }); + } + if (state.isNothing) { + return Nothing(); + } + return Just(state.value); +}; + + // Just.prototype.datatype = Just; diff --git a/src/internal/util.js b/src/internal/util.js index ba517c4..4da94ad 100644 --- a/src/internal/util.js +++ b/src/internal/util.js @@ -38,6 +38,18 @@ module.exports = { returnThis: function() { return this; }, + chainRecNext: function(v) { + return { done: false, value: v}; + }, + + chainRecDone: function(v) { + return { done: true, value: v}; + }, + + chainRecFold: function(v, onNext, onDone) { + return (v.done ? onDone : onNext)(v.value); + }, + deriveAp: function (Type) { return function(fa) { return this.chain(function (f) { diff --git a/test/maybe.test.js b/test/maybe.test.js index ac8eb4a..3550c9d 100644 --- a/test/maybe.test.js +++ b/test/maybe.test.js @@ -75,6 +75,48 @@ describe('Maybe', function() { jsv.assert(jsv.forall(m, f, f, env, cTest.associative)); }); + describe('ChainRec', function() { + it('is a ChainRec', function() { + var cTest = types.chainRec; + var predicate = function(a) { + return a.length > 5; + }; + var done = Maybe.of; + var x = 1; + var initial = [x]; + var next = function(a) { + return Maybe.of(a.concat([x])); + }; + assert.equal(true, cTest.iface(Maybe.of(1))); + return cTest.equivalence(Maybe, predicate, done, next, initial); + }); + + it('is stacksafe', function() { + return Maybe.of('DONE').equals(Maybe.chainRec(function(next, done, n) { + if (n === 0) { + return Maybe.of(done('DONE')); + } else { + return Maybe.of(next(n - 1)); + } + }, 100000)); + }); + + it('fail Immediately', function() { + return Maybe.Nothing().equals(Maybe.chainRec(function(/*next, done, n*/) { + return Maybe.Nothing(); + }, 100)); + }); + + it('fail on next step', function() { + return Maybe.Nothing().equals(Maybe.chainRec(function(next, done, n) { + if (n === 0) { + return Maybe.Nothing(); + } + return Maybe.of(next(n - 1)); + }, 100)); + }); + }); + it('is a Monad', function() { var mTest = types.monad; From 5b12e4b99c14f4c90abedcc0fc45893513ae35e4 Mon Sep 17 00:00:00 2001 From: Irakli Safareli Date: Thu, 8 Sep 2016 20:08:56 +0400 Subject: [PATCH 06/11] use util.chainRec helpers in Future and Identity --- src/Future.js | 40 ++++++++++++++++------------------------ src/Identity.js | 12 ++---------- 2 files changed, 18 insertions(+), 34 deletions(-) diff --git a/src/Future.js b/src/Future.js index 35634aa..becfef6 100644 --- a/src/Future.js +++ b/src/Future.js @@ -3,6 +3,8 @@ var forEach = require('ramda/src/forEach'); var toString = require('ramda/src/toString'); var curry = require('ramda/src/curry'); +var util = require('./internal/util'); + function jail(handler, f){ return function(a){ try{ @@ -101,44 +103,34 @@ var chainRecFork = function(t, rej, res) { } }; -var chainRecNext = function(v) { - return function(onNext/*, onDone*/){ - return onNext(v); - }; -}; - -var chainRecDone = function(v) { - return function(onNext, onDone){ - return onDone(v); - }; -}; - // chainRec Future.chainRec = Future.prototype.chainRec = function(f, i) { return new Future(function(reject, resolve) { chainRecFork( - f(chainRecNext, chainRecDone, i), + f(util.chainRecNext, util.chainRecDone, i), function(z/*, isSync*/) { return reject(z); }, - function(fold, isSync) { - return fold( - function(v2) { + function(step, isSync) { + return util.chainRecFold( + step, + function(v) { if (isSync === false) { - Future.chainRec(f, v2).fork(reject, resolve); + Future.chainRec(f, v).fork(reject, resolve); return; } - var state = { loop: true, arg: v2 }; + var state = { loop: true, arg: v }; var onReject = function(z/*, isSync*/) { state.loop = false; reject(z); }; - var onResolve = function(fold, isSync) { - return fold( - function(v3) { - state = { loop: isSync, arg: v3 }; + var onResolve = function(step, isSync) { + return util.chainRecFold( + step, + function(v2) { + state = { loop: isSync, arg: v2 }; if (isSync === false) { - Future.chainRec(f, v3).fork(reject, resolve); + Future.chainRec(f, v2).fork(reject, resolve); } }, function(v) { @@ -149,7 +141,7 @@ Future.chainRec = Future.prototype.chainRec = function(f, i) { }; while (state.loop) { var forkRes = chainRecFork( - f(chainRecNext, chainRecDone, state.arg), + f(util.chainRecNext, util.chainRecDone, state.arg), onReject, onResolve ); diff --git a/src/Identity.js b/src/Identity.js index a18dc97..94bb2e5 100644 --- a/src/Identity.js +++ b/src/Identity.js @@ -68,19 +68,11 @@ Identity.prototype.chain = function(fn) { return fn(this.value); }; -var chainRecNext = function(v) { - return { done: false, value: v}; -}; - -var chainRecDone = function(v) { - return { done: true, value: v}; -}; - // chainRec Identity.chainRec = Identity.prototype.chainRec = function(f, i) { - var state = { done:false, value: i}; + var state = util.chainRecNext(i); while(state.done === false) { - state = f(chainRecNext, chainRecDone, state.value).get(); + state = f(util.chainRecNext, util.chainRecDone, state.value).get(); } return Identity(state.value); }; From bedc596a1cc1cd3c38e345b13b24ebce732fbaeb Mon Sep 17 00:00:00 2001 From: Irakli Safareli Date: Fri, 9 Sep 2016 00:59:38 +0400 Subject: [PATCH 07/11] implement ChainRec for Either --- src/Either.js | 17 +++++++++++++++++ test/either.test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/Either.js b/src/Either.js index a5fe269..099625f 100644 --- a/src/Either.js +++ b/src/Either.js @@ -70,6 +70,23 @@ _Right.prototype.chain = function(f) { return f(this.value); }; +//chainRec +Either.chainRec = Either.prototype.chainRec = function(f, i) { + var state = util.chainRecNext(i); + while (state.done === false) { + state = Either.either( + function(v) { + return { done: true, result: v, isLeft: true }; + }, + function(v) { + return v; + }, + f(util.chainRecNext, util.chainRecDone, state.value) + ); + } + return (state.isLeft ? Either.Left : Either.Right)(state.value); +}; + _Right.prototype.bimap = function(_, f) { return new _Right(f(this.value)); }; diff --git a/test/either.test.js b/test/either.test.js index 785a665..272991b 100644 --- a/test/either.test.js +++ b/test/either.test.js @@ -49,6 +49,48 @@ describe('Either', function() { jsv.assert(jsv.forall(eNatArb, fnEArb, fnEArb, cTest.associative)); }); + describe('ChainRec', function() { + it('is a ChainRec', function() { + var cTest = types.chainRec; + var predicate = function(a) { + return a.length > 5; + }; + var done = Either.of; + var x = 1; + var initial = [x]; + var next = function(a) { + return Either.of(a.concat([x])); + }; + assert.equal(true, cTest.iface(Either.of(1))); + return cTest.equivalence(Either, predicate, done, next, initial); + }); + + it('is stacksafe', function() { + return Either.of('DONE').equals(Either.chainRec(function(next, done, n) { + if (n === 0) { + return Either.of(done('DONE')); + } else { + return Either.of(next(n - 1)); + } + }, 100000)); + }); + + it('fail Immediately', function() { + return Either.Left("ERROR").equals(Either.chainRec(function(/*next, done, n*/) { + return Either.Left("ERROR"); + }, 100)); + }); + + it('fail on next step', function() { + return Either.Left("ERROR").equals(Either.chainRec(function(next, done, n) { + if (n === 0) { + return Either.Left("ERROR"); + } + return Either.of(next(n - 1)); + }, 100)); + }); + }); + it('is a Monad', function() { jsv.assert(jsv.forall(eNatArb, types.monad.iface)); }); From 2e82ce27d9ed1b66526e3450cd026bf5242e018d Mon Sep 17 00:00:00 2001 From: Irakli Safareli Date: Fri, 9 Sep 2016 09:36:58 +0400 Subject: [PATCH 08/11] implement ChainRec for IO --- src/IO.js | 13 +++++++++++++ test/io.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/IO.js b/src/IO.js index 570c4b3..a1e4dbc 100644 --- a/src/IO.js +++ b/src/IO.js @@ -1,6 +1,8 @@ var compose = require('ramda/src/compose'); var toString = require('ramda/src/toString'); +var util = require('./internal/util'); + module.exports = IO; function IO(fn) { @@ -21,6 +23,17 @@ IO.prototype.chain = function(f) { }); }; +//chainRec +IO.chainRec = IO.prototype.chainRec = function(f, i) { + return new IO(function() { + var state = util.chainRecNext(i); + while (state.done === false) { + state = f(util.chainRecNext, util.chainRecDone, state.value).fn(); + } + return state.value; + }); +}; + IO.prototype.map = function(f) { var io = this; return new IO(compose(f, io.fn)); diff --git a/test/io.test.js b/test/io.test.js index 4288b88..881fb80 100644 --- a/test/io.test.js +++ b/test/io.test.js @@ -5,6 +5,10 @@ var types = require('./types')(function(io1, io2) { var IO = require('..').IO; +IO.prototype.equals = function(b) { + assert.deepEqual(this.fn(), b.fn()); +}; + function add(a) { return function(b) { return a + b; }; } @@ -85,6 +89,33 @@ describe('IO', function() { assert.equal(true, cTest.associative(c, identity, identity)); }); + describe('ChainRec', function() { + it('is a ChainRec', function() { + var cTest = types.chainRec; + var predicate = function(a) { + return a.length > 5; + }; + var done = IO.of; + var x = 1; + var initial = [x]; + var next = function(a) { + return IO.of(a.concat([x])); + }; + assert.equal(true, cTest.iface(IO.of(1))); + return cTest.equivalence(IO, predicate, done, next, initial); + }); + + it('is stacksafe', function() { + return IO.of('DONE').equals(IO.chainRec(function(next, done, n) { + if (n === 0) { + return IO.of(done('DONE')); + } else { + return IO.of(next(n - 1)); + } + }, 100000)); + }); + }); + it('is a Monad', function() { var mTest = types.monad; assert.equal(true, mTest.iface(i1)); From 3a73bdfd768603e68c23f6aa9dac099d1418d48b Mon Sep 17 00:00:00 2001 From: Irakli Safareli Date: Sat, 10 Sep 2016 18:11:15 +0400 Subject: [PATCH 09/11] fix some test issues and update README --- README.md | 2 +- src/Either.js | 2 +- src/Maybe.js | 4 ++-- test/either.test.js | 14 +++++++------- test/identity.test.js | 6 +++--- test/io.test.js | 11 ++++++----- test/maybe.test.js | 12 +++++++----- 7 files changed, 27 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index b14b4d3..9105b34 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ not undergone thorough testing/use yet. | [Identity][11] | **✔︎** | | **✔︎** | **✔︎** | **✔︎** | | **✔︎** | | [IO][12] | | | **✔︎** | **✔︎** | **✔︎** | | **✔︎** | | [Maybe][13] | **✔︎** | | **✔︎** | **✔︎** | **✔︎** | **✔︎** | **✔︎** | -| [Reader][14] | | | **✔︎** | **✔︎** | **✔︎** | | **✔︎** | +| [Reader][14] | | | **✔︎** | **✔︎** | **✔︎** | | | | [Tuple][15] | **✔︎** | **✔︎** | **✔︎** | | | | | diff --git a/src/Either.js b/src/Either.js index 099625f..3d8ffce 100644 --- a/src/Either.js +++ b/src/Either.js @@ -76,7 +76,7 @@ Either.chainRec = Either.prototype.chainRec = function(f, i) { while (state.done === false) { state = Either.either( function(v) { - return { done: true, result: v, isLeft: true }; + return { done: true, value: v, isLeft: true }; }, function(v) { return v; diff --git a/src/Maybe.js b/src/Maybe.js index 8a51d06..30567c1 100644 --- a/src/Maybe.js +++ b/src/Maybe.js @@ -87,9 +87,9 @@ Maybe.chainRec = Maybe.prototype.chainRec = function(f, i) { state = f(util.chainRecNext, util.chainRecDone, state.value).getOrElse({ done: true, isNothing:true }); } if (state.isNothing) { - return Nothing(); + return Maybe.Nothing(); } - return Just(state.value); + return Maybe.Just(state.value); }; diff --git a/test/either.test.js b/test/either.test.js index 272991b..b8773d7 100644 --- a/test/either.test.js +++ b/test/either.test.js @@ -62,32 +62,32 @@ describe('Either', function() { return Either.of(a.concat([x])); }; assert.equal(true, cTest.iface(Either.of(1))); - return cTest.equivalence(Either, predicate, done, next, initial); + assert.equal(true, cTest.equivalence(Either, predicate, done, next, initial)); }); it('is stacksafe', function() { - return Either.of('DONE').equals(Either.chainRec(function(next, done, n) { + assert.equal(true, Either.of('DONE').equals(Either.chainRec(function(next, done, n) { if (n === 0) { return Either.of(done('DONE')); } else { return Either.of(next(n - 1)); } - }, 100000)); + }, 100000))); }); it('fail Immediately', function() { - return Either.Left("ERROR").equals(Either.chainRec(function(/*next, done, n*/) { + assert.equal(true, Either.Left("ERROR").equals(Either.chainRec(function(/*next, done, n*/) { return Either.Left("ERROR"); - }, 100)); + }, 100))); }); it('fail on next step', function() { - return Either.Left("ERROR").equals(Either.chainRec(function(next, done, n) { + assert.equal(true, Either.Left("ERROR").equals(Either.chainRec(function(next, done, n) { if (n === 0) { return Either.Left("ERROR"); } return Either.of(next(n - 1)); - }, 100)); + }, 100))); }); }); diff --git a/test/identity.test.js b/test/identity.test.js index cd68450..57f9553 100644 --- a/test/identity.test.js +++ b/test/identity.test.js @@ -70,17 +70,17 @@ describe('Identity', function() { return Identity.of(a.concat([x])); }; assert.equal(true, cTest.iface(Identity.of(1))); - return cTest.equivalence(Identity, predicate, done, next, initial); + assert.equal(true, cTest.equivalence(Identity, predicate, done, next, initial)); }); it('is stacksafe', function() { - return Identity.of('DONE').equals(Identity.chainRec(function(next, done, n) { + assert.equal(true, Identity.of('DONE').equals(Identity.chainRec(function(next, done, n) { if (n === 0) { return Identity.of(done('DONE')); } else { return Identity.of(next(n - 1)); } - }, 100000)); + }, 100000))); }); }); diff --git a/test/io.test.js b/test/io.test.js index 881fb80..3b8cfe7 100644 --- a/test/io.test.js +++ b/test/io.test.js @@ -1,12 +1,13 @@ var assert = require('assert'); +var equals = require('ramda/src/equals'); var types = require('./types')(function(io1, io2) { - return io1.runIO('x') === io2.runIO('x'); + return io1.equals(io2); }); var IO = require('..').IO; IO.prototype.equals = function(b) { - assert.deepEqual(this.fn(), b.fn()); + return equals(this.runIO('x'), b.runIO('x')); }; function add(a) { @@ -102,17 +103,17 @@ describe('IO', function() { return IO.of(a.concat([x])); }; assert.equal(true, cTest.iface(IO.of(1))); - return cTest.equivalence(IO, predicate, done, next, initial); + assert.equal(true, cTest.equivalence(IO, predicate, done, next, initial)); }); it('is stacksafe', function() { - return IO.of('DONE').equals(IO.chainRec(function(next, done, n) { + assert.equal(true, IO.of('DONE').equals(IO.chainRec(function(next, done, n) { if (n === 0) { return IO.of(done('DONE')); } else { return IO.of(next(n - 1)); } - }, 100000)); + }, 100000))); }); }); diff --git a/test/maybe.test.js b/test/maybe.test.js index 3550c9d..ec1ce95 100644 --- a/test/maybe.test.js +++ b/test/maybe.test.js @@ -88,23 +88,25 @@ describe('Maybe', function() { return Maybe.of(a.concat([x])); }; assert.equal(true, cTest.iface(Maybe.of(1))); - return cTest.equivalence(Maybe, predicate, done, next, initial); + assert.equal(true, cTest.equivalence(Maybe, predicate, done, next, initial)); }); it('is stacksafe', function() { - return Maybe.of('DONE').equals(Maybe.chainRec(function(next, done, n) { + var a = Maybe.chainRec(function(next, done, n) { if (n === 0) { return Maybe.of(done('DONE')); } else { return Maybe.of(next(n - 1)); } - }, 100000)); + }, 100000); + console.log('a',a); + assert.equal(true, Maybe.of('DONE').equals(a)); }); it('fail Immediately', function() { - return Maybe.Nothing().equals(Maybe.chainRec(function(/*next, done, n*/) { + assert.equal(true, Maybe.Nothing().equals(Maybe.chainRec(function(/*next, done, n*/) { return Maybe.Nothing(); - }, 100)); + }, 100))); }); it('fail on next step', function() { From 8e9948c1ee703ab5a962e20201ca384735cf022d Mon Sep 17 00:00:00 2001 From: Irakli Safareli Date: Tue, 13 Sep 2016 01:18:17 +0400 Subject: [PATCH 10/11] refactored ChainRec implementation and it's ADT --- src/Either.js | 20 ++++---- src/Future.js | 108 ++++++++++++++++--------------------------- src/IO.js | 2 +- src/Identity.js | 2 +- src/Maybe.js | 13 +++--- src/internal/util.js | 8 +--- 6 files changed, 59 insertions(+), 94 deletions(-) diff --git a/src/Either.js b/src/Either.js index 3d8ffce..8d6c1bb 100644 --- a/src/Either.js +++ b/src/Either.js @@ -72,19 +72,15 @@ _Right.prototype.chain = function(f) { //chainRec Either.chainRec = Either.prototype.chainRec = function(f, i) { - var state = util.chainRecNext(i); - while (state.done === false) { - state = Either.either( - function(v) { - return { done: true, value: v, isLeft: true }; - }, - function(v) { - return v; - }, - f(util.chainRecNext, util.chainRecDone, state.value) - ); + var res, state = util.chainRecNext(i); + while (state.isNext) { + res = f(util.chainRecNext, util.chainRecDone, state.value); + if (Either.isLeft(res)) { + return res; + } + state = res.value; } - return (state.isLeft ? Either.Left : Either.Right)(state.value); + return Either.Right(state.value); }; _Right.prototype.bimap = function(_, f) { diff --git a/src/Future.js b/src/Future.js index becfef6..4d8dd7e 100644 --- a/src/Future.js +++ b/src/Future.js @@ -84,76 +84,48 @@ Future.prototype.chain = function(f) { // Sorella's: }.bind(this)); }; -var chainRecFork = function(t, rej, res) { - var isSync = false; - t.fork(function(v) { - var r = rej(v, !isSync); - isSync = true; - return r; - }, function(v) { - var r = res(v, !isSync); - isSync = true; - return r; - }); - if (isSync) { - return { isSync: true }; - } else { - isSync = true; - return { isSync: false }; - } -}; - // chainRec -Future.chainRec = Future.prototype.chainRec = function(f, i) { - return new Future(function(reject, resolve) { - chainRecFork( - f(util.chainRecNext, util.chainRecDone, i), - function(z/*, isSync*/) { - return reject(z); - }, - function(step, isSync) { - return util.chainRecFold( - step, - function(v) { - if (isSync === false) { - Future.chainRec(f, v).fork(reject, resolve); - return; - } - var state = { loop: true, arg: v }; - var onReject = function(z/*, isSync*/) { - state.loop = false; - reject(z); - }; - var onResolve = function(step, isSync) { - return util.chainRecFold( - step, - function(v2) { - state = { loop: isSync, arg: v2 }; - if (isSync === false) { - Future.chainRec(f, v2).fork(reject, resolve); - } - }, - function(v) { - state = { loop: false}; - resolve(v); - } - ); - }; - while (state.loop) { - var forkRes = chainRecFork( - f(util.chainRecNext, util.chainRecDone, state.arg), - onReject, - onResolve - ); - if (forkRes.isSync === false) { - state = { loop: false}; - } - } - }, - resolve - ); +// +// Heavily influenced by the Aff MonadRec instance +// https://github.com/slamdata/purescript-aff/blob/51106474122d0e5aec8e3d5da5bb66cfe8062f55/src/Control/Monad/Aff.js#L263-L322 +Future.chainRec = Future.prototype.chainRec = function(f, a) { + return Future(function(reject, resolve) { + return function go(acc) { + // isSync could be in three possable states + // * null - unresolved status + // * true - synchronous future + // * false - asynchronous future + var isSync = null; + var state = util.chainRecNext(acc); + var onResolve = function(v) { + // If the `isSync` is still unresolved, we have observed a + // synchronous future. Otherwise, `isSync` will be `false`. + if (isSync === null) { + isSync = true; + // Store the result for further synchronous processing. + state = v; + } else { + // When we have observed an asynchronous future, we use normal + // recursion. This is safe because we will be on a new stack. + (v.isNext ? go : resolve)(v.value); + } + }; + while (state.isNext) { + isSync = null; + f(util.chainRecNext, util.chainRecDone, state.value).fork(reject, onResolve); + // If the `isSync` has already resolved to `true` by our `onResolve`, then + // we have observed a synchronous future. Otherwise it will still be `null`. + if (isSync === true) { + continue; + } else { + // If the status has not resolved yet, then we have observed an + // asynchronous or failed future so update status and exit the loop. + isSync = false; + return; + } } - ); + resolve(state.value); + }(a); }); }; diff --git a/src/IO.js b/src/IO.js index a1e4dbc..28b87cb 100644 --- a/src/IO.js +++ b/src/IO.js @@ -27,7 +27,7 @@ IO.prototype.chain = function(f) { IO.chainRec = IO.prototype.chainRec = function(f, i) { return new IO(function() { var state = util.chainRecNext(i); - while (state.done === false) { + while (state.isNext) { state = f(util.chainRecNext, util.chainRecDone, state.value).fn(); } return state.value; diff --git a/src/Identity.js b/src/Identity.js index 94bb2e5..4744323 100644 --- a/src/Identity.js +++ b/src/Identity.js @@ -71,7 +71,7 @@ Identity.prototype.chain = function(fn) { // chainRec Identity.chainRec = Identity.prototype.chainRec = function(f, i) { var state = util.chainRecNext(i); - while(state.done === false) { + while (state.isNext) { state = f(util.chainRecNext, util.chainRecDone, state.value).get(); } return Identity(state.value); diff --git a/src/Maybe.js b/src/Maybe.js index 30567c1..a8b8f42 100644 --- a/src/Maybe.js +++ b/src/Maybe.js @@ -82,12 +82,13 @@ Nothing.prototype.chain = util.returnThis; //chainRec Maybe.chainRec = Maybe.prototype.chainRec = function(f, i) { - var state = util.chainRecNext(i); - while (state.done === false) { - state = f(util.chainRecNext, util.chainRecDone, state.value).getOrElse({ done: true, isNothing:true }); - } - if (state.isNothing) { - return Maybe.Nothing(); + var res, state = util.chainRecNext(i); + while (state.isNext) { + res = f(util.chainRecNext, util.chainRecDone, state.value); + if (Maybe.isNothing(res)) { + return res; + } + state = res.value; } return Maybe.Just(state.value); }; diff --git a/src/internal/util.js b/src/internal/util.js index 4da94ad..d520e43 100644 --- a/src/internal/util.js +++ b/src/internal/util.js @@ -39,15 +39,11 @@ module.exports = { returnThis: function() { return this; }, chainRecNext: function(v) { - return { done: false, value: v}; + return { isNext: true, value: v }; }, chainRecDone: function(v) { - return { done: true, value: v}; - }, - - chainRecFold: function(v, onNext, onDone) { - return (v.done ? onDone : onNext)(v.value); + return { isNext: false, value: v }; }, deriveAp: function (Type) { From 5318f1544ee5da1dafd8bfa8c41fd928fabeb92f Mon Sep 17 00:00:00 2001 From: Irakli Safareli Date: Fri, 16 Sep 2016 16:38:03 +0400 Subject: [PATCH 11/11] update test descriptions --- test/either.test.js | 4 ++-- test/future.test.js | 6 +++--- test/maybe.test.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/either.test.js b/test/either.test.js index b8773d7..ddb6a16 100644 --- a/test/either.test.js +++ b/test/either.test.js @@ -75,13 +75,13 @@ describe('Either', function() { }, 100000))); }); - it('fail Immediately', function() { + it('responds to failure immediately', function() { assert.equal(true, Either.Left("ERROR").equals(Either.chainRec(function(/*next, done, n*/) { return Either.Left("ERROR"); }, 100))); }); - it('fail on next step', function() { + it('responds to failure on next step', function() { assert.equal(true, Either.Left("ERROR").equals(Either.chainRec(function(next, done, n) { if (n === 0) { return Either.Left("ERROR"); diff --git a/test/future.test.js b/test/future.test.js index 151dcee..c41b4a5 100644 --- a/test/future.test.js +++ b/test/future.test.js @@ -119,7 +119,7 @@ describe('Future', function() { return cTest.equivalence(Future, predicate, done, next, initial); }); - it('sync and async Futures', function() { + it('works when mixing sync and async Futures', function() { return Future.of('DONE').equals(Future.chainRec(function(next, done, n) { if (n === 0) { return Future.of(done('DONE')); @@ -131,13 +131,13 @@ describe('Future', function() { }, 100000)); }); - it('fail Immediately', function() { + it('responds to failure immediately', function() { return Future.reject('ERROR').equals(Future.chainRec(function(/*next, done, n*/) { return Future.reject('ERROR'); }, 100)); }); - it('fail on next step', function() { + it('responds to failure on next step', function() { return Future.reject('ERROR').equals(Future.chainRec(function(next, done, n) { if (n === 0) { return Future.reject('ERROR'); diff --git a/test/maybe.test.js b/test/maybe.test.js index ec1ce95..5538eef 100644 --- a/test/maybe.test.js +++ b/test/maybe.test.js @@ -103,13 +103,13 @@ describe('Maybe', function() { assert.equal(true, Maybe.of('DONE').equals(a)); }); - it('fail Immediately', function() { + it('responds to failure immediately', function() { assert.equal(true, Maybe.Nothing().equals(Maybe.chainRec(function(/*next, done, n*/) { return Maybe.Nothing(); }, 100))); }); - it('fail on next step', function() { + it('responds to failure on next step', function() { return Maybe.Nothing().equals(Maybe.chainRec(function(next, done, n) { if (n === 0) { return Maybe.Nothing();