Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement FantasyLand ChainRec #125

Merged
merged 11 commits into from
Sep 16, 2016
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
13 changes: 13 additions & 0 deletions src/Either.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ _Right.prototype.chain = function(f) {
return f(this.value);
};

//chainRec
Either.chainRec = Either.prototype.chainRec = function(f, i) {
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 Either.Right(state.value);
};

_Right.prototype.bimap = function(_, f) {
return new _Right(f(this.value));
};
Expand Down
47 changes: 47 additions & 0 deletions src/Future.js
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -82,6 +84,51 @@ Future.prototype.chain = function(f) { // Sorella's:
}.bind(this));
};

// chainRec
//
// 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);
});
};

// chainReject
// Like chain but operates on the reject instead of the resolve case.
//:: Future a, b => (a -> Future c) -> Future c
Expand Down
13 changes: 13 additions & 0 deletions src/IO.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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.isNext) {
state = f(util.chainRecNext, util.chainRecDone, state.value).fn();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise here, we could use Left and Right instead of chainRecNext and chainRecDone, along with Either.isLeft(state) instead of state.done === false.

}
return state.value;
});
};

IO.prototype.map = function(f) {
var io = this;
return new IO(compose(f, io.fn));
Expand Down
9 changes: 9 additions & 0 deletions src/Identity.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ Identity.prototype.chain = function(fn) {
return fn(this.value);
};

// chainRec
Identity.chainRec = Identity.prototype.chainRec = function(f, i) {
var state = util.chainRecNext(i);
while (state.isNext) {
state = f(util.chainRecNext, util.chainRecDone, state.value).get();
}
return Identity(state.value);
};

/**
* Returns the value of `Identity[a]`
*
Expand Down
14 changes: 14 additions & 0 deletions src/Maybe.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ Just.prototype.chain = util.baseMap;
Nothing.prototype.chain = util.returnThis;


//chainRec
Maybe.chainRec = Maybe.prototype.chainRec = function(f, i) {
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);
};


//
Just.prototype.datatype = Just;

Expand Down
8 changes: 8 additions & 0 deletions src/internal/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ module.exports = {

returnThis: function() { return this; },

chainRecNext: function(v) {
return { isNext: true, value: v };
},

chainRecDone: function(v) {
return { isNext: false, value: v };
},

deriveAp: function (Type) {
return function(fa) {
return this.chain(function (f) {
Expand Down
42 changes: 42 additions & 0 deletions test/either.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
assert.equal(true, cTest.equivalence(Either, predicate, done, next, initial));
});

it('is stacksafe', function() {
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)));
});

it('fail Immediately', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd prefer if these it statements read like English, e.g. it("responds to failure immediately") etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

assert.equal(true, Either.Left("ERROR").equals(Either.chainRec(function(/*next, done, n*/) {
return Either.Left("ERROR");
}, 100)));
});

it('fail on next step', function() {
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)));
});
});

it('is a Monad', function() {
jsv.assert(jsv.forall(eNatArb, types.monad.iface));
});
Expand Down
Loading