Skip to content

Commit

Permalink
feat(stack): make foldMap stacksafe
Browse files Browse the repository at this point in the history
now target Monad should also be ChainRec so that foldMap could be stacksafe

Closes #22

BREAKING CHANGE

`retract` and `foldMap` now are taking Type containing `of` and `chainRec` methods instead of a function
  • Loading branch information
safareli committed Sep 18, 2016
1 parent d4db7b4 commit 0f3fbbf
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 35 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ Free implements [Functor](https://github.com/fantasyland/fantasy-land#functor),

- Free.prototype.`hoist :: Free i a -> (i -> z) -> Free z a`
- Free.`liftF :: i -> Free i a`
- Free.prototype.`retract :: Monad m => Free m a -> (a -> m a) -> m a`
- Free.prototype.`retract :: (ChainRec m, Monad m) => Free m a -> m -> m a`
- Free.prototype.`graft :: Free i a -> (i -> Free z a) -> Free z a`
- Free.prototype.`foldMap :: Monad m => Free i a -> (i -> m a) -> (a -> m a) -> m a`
- Free.prototype.`foldMap :: (ChainRec m, Monad m) => Free i a -> (i -> m a) -> m -> m a`

### Free structure function equivalencies:

- `graft(f) ≡ foldMap(f, Free.of)`
- `hoist(f) ≡ foldMap(compose(liftF, f), Free.of)`
- `retract(of) ≡ foldMap(id, of)`
- `foldMap(f, of) ≡ compose(retract(of), hoist(f))`
- `graft(f) ≡ foldMap(f, Free)`
- `hoist(f) ≡ foldMap(compose(liftF, f), Free)`
- `retract(M) ≡ foldMap(id, M)`
- `foldMap(f, M) ≡ compose(retract(M), hoist(f))`

---

Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,18 @@
"commitizen": "2.8.2",
"conventional-recommended-bump": "0.2.1",
"cz-conventional-changelog": "1.1.6",
"data.task": "3.1.0",
"eslint": "2.13.1",
"eslint-config-standard": "5.3.1",
"eslint-plugin-promise": "1.3.2",
"eslint-plugin-standard": "1.3.2",
"fantasy-combinators": "0.0.1",
"fantasy-identities": "0.0.1",
"fantasy-land": "0.2.1",
"ghooks": "1.3.0",
"jsverify": "0.7.1",
"nodemon": "1.9.2",
"npm-run-all": "2.3.0",
"ramda": "0.21.0",
"ramda-fantasy": "git@github.com:ramda/ramda-fantasy.git#8675e8086afada93390df0957a1e73a004e878d2",
"rimraf": "2.5.3",
"semantic-release": "^4.3.5",
"tap": "6.1.1",
Expand Down
29 changes: 18 additions & 11 deletions src/free.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,24 +123,31 @@ Free.prototype.chain = function(f) {
}

Free.prototype.hoist = function(f) {
return this.foldMap(compose(Free.liftF, f), Free.of)
return this.foldMap(compose(Free.liftF, f), Free)
}

Free.prototype.retract = function(of) {
return this.foldMap(id, of)
Free.prototype.retract = function(m) {
return this.foldMap(id, m)
}

Free.prototype.foldMap = function(f, of) {
return this.cata({
Pure: (x) => of(x),
Lift: (x, g) => f(x).map(g),
Ap: (x, y) => y.foldMap(f, of).ap(x.foldMap(f, of)),
Chain: (x, g) => x.foldMap(f, of).chain((a) => g(a).foldMap(f, of)),
})
Free.prototype.foldMap = function(f, m) {
return m.chainRec((next, done, v) => v.cata({
Pure: (x) => m.of(x).map(done),
Lift: (x, g) => f(x).map(g).map(done),
Ap: (x, y) => y.foldMap(f, m).ap(x.foldMap(f, m)).map(done),
Chain: (x, g) => x.foldMap(f, m).map(g).map(next),
}), this)
}

Free.prototype.graft = function(f) {
return this.foldMap(f, Free.of)
return this.foldMap(f, Free)
}

const chainRecNext = (value) => ({ done: false, value })
const chainRecDone = (value) => ({ done: true, value })

Free.chainRec = (f, i) => f(chainRecNext, chainRecDone, i).chain(
({ done, value }) => done ? Free.of(value) : Free.chainRec(f, value)
)

module.exports = Free
7 changes: 3 additions & 4 deletions test/concurrency.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const { test } = require('tap')
const { Free } = require('./lib')
const Task = require('data.task')
const { Free, Future } = require('./lib')

test('Check for concurrency', (t) => {
const shout = (tag, ms) => Free.liftF({tag: `${tag}.${ms}`, ms})
Expand Down Expand Up @@ -28,13 +27,13 @@ test('Check for concurrency', (t) => {
)
.ap(shout('out.ap', 10))

.foldMap(({tag, ms}) => new Task((rej, res) => {
.foldMap(({tag, ms}) => Future((rej, res) => {
orders.start.push(tag)
setTimeout(() => {
orders.end.push(tag)
res(tag)
}, ms)
}), Task.of)
}), Future)
.fork(() => {}, (result) => {
t.same(orders, {
end: [
Expand Down
30 changes: 22 additions & 8 deletions test/laws.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
const = require('jsverify')
const Identity = require('fantasy-identities')
const { test } = require('tap')
const { Free } = require('./lib')
const { Free, Identity, Future } = require('./lib')
const equals = require('ramda/src/equals')
const lawMonad = require('fantasy-land/laws/monad.js')
const lawApplicative = require('fantasy-land/laws/applicative.js')
Expand All @@ -23,6 +22,21 @@ test('Check laws', (t) => {
t.end()
})

test('Is stack safe', t => {
const runTimes = (n) => (v) => {
const res = Free.liftF(n)
if (n === 0) {
return res
}
return res.chain(runTimes(n - 1))
}

runTimes(10000)().foldMap(Future.of, Future).fork(t.error, (v) => t.equals(v, 0, 'Works with Future'))
t.equals(runTimes(10000)().foldMap(Identity.of, Identity).get(), 0, 'Works with Identity')

t.end()
})

test('Check Free structure function equivalencies', (t) => {
const compose = (f, g) => (x) => f(g(x))
const id = x => x
Expand All @@ -31,23 +45,23 @@ test('Check Free structure function equivalencies', (t) => {
const treeEq = (a, b) => equals(foldTree(a), foldTree(b))

const cases = [
['graft(f) ≡ foldMap(f, Free.of)', .forall('number -> number', (f) => {
['graft(f) ≡ foldMap(f, Free)', .forall('number -> number', (f) => {
const = compose(Free.liftF, f)
return treeEq(tree.graft(), tree.foldMap(, Free.of))
return treeEq(tree.graft(), tree.foldMap(, Free))
})],

// map(f) ≡ chain(compose(of, f))
// hoist(f) ≡ graft(compose(liftF, f))
['hoist(f) ≡ foldMap(compose(liftF, f), Free.of)', .forall('number -> number', (f) => {
return treeEq(tree.hoist(f), tree.foldMap(compose(Free.liftF, f), Free.of))
['hoist(f) ≡ foldMap(compose(liftF, f), Free)', .forall('number -> number', (f) => {
return treeEq(tree.hoist(f), tree.foldMap(compose(Free.liftF, f), Free))
})],

['retract(of) ≡ foldMap(id, of)', .forall('number -> number', (f) => {
['retract(M) ≡ foldMap(id, M)', .forall('number -> number', (f) => {
const treeʹ = tree.hoist(compose(Identity, f))
return equals(treeʹ.retract(Identity).x, treeʹ.foldMap(id, Identity).x)
})],

['foldMap(f, of) ≡ compose(retract(of), hoist(f))', .forall('number -> number', (f) => {
['foldMap(f, M) ≡ compose(retract(M), hoist(f))', .forall('number -> number', (f) => {
const = compose(Identity, f)
return equals(tree.foldMap(, Identity).x, tree.hoist().retract(Identity).x)
})],
Expand Down
4 changes: 4 additions & 0 deletions test/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
const = require('jsverify')
const Free = require('../../src/free.js')
const { Future, Identity } = require('ramda-fantasy')

.any = .oneof(.falsy, .json)

module.exports = {
Free,
Future,
Identity,
}
7 changes: 3 additions & 4 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const Identity = require('fantasy-identities')
const { test } = require('tap')
const { Free } = require('./lib')
const { Free, Identity } = require('./lib')

test('Free', t => {
t.throws(() => Free(), 'Calling Free directly throws')
Expand All @@ -15,8 +14,8 @@ test('hoist', t => {
)

t.deepEqual(
tree.hoist((a) => a + 2).foldMap((a) => Identity(a), Identity).x,
[3, 4],
tree.hoist((a) => a + 2).foldMap((a) => Identity(a), Identity),
Identity.of([3, 4]),
'should add 2 to all instructions in Free'
)
t.end()
Expand Down

0 comments on commit 0f3fbbf

Please sign in to comment.