Skip to content

Commit

Permalink
Merge branch 'master' into feature/compose2
Browse files Browse the repository at this point in the history
  • Loading branch information
evilsoft committed Jul 21, 2019
2 parents 6258df2 + e8cf52d commit 1d13450
Show file tree
Hide file tree
Showing 12 changed files with 342 additions and 5 deletions.
84 changes: 83 additions & 1 deletion docs/src/pages/docs/crocks/Async.md
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ Async e a ~> (a -> Async e b) -> Async e b

Combining a sequential series of transformations that capture disjunction can be
accomplished with `chain`. `chain` expects a unary, `Async` returning function
as its argument. When invoked on a [`Rejected`](#rejected) instance , `chain` will not run
as its argument. When invoked on a [`Rejected`](#rejected) instance, `chain` will not run
the function, but will instead return another [`Rejected`](#rejected) instance wrapping the
original [`Rejected`](#rejected) value. When called on a [`Resolved`](#resolved) instance however, the
inner value will be passed to provided function, returning the result as the
Expand Down Expand Up @@ -966,6 +966,88 @@ resolve(Rejected('Rejected'))
//=> res: "Was Rejected"
```

#### bichain

```haskell
Async e a ~> ((e -> Async b c), (a -> Async b c)) -> Async b c
```

Combining a sequential series of transformations that capture disjunction can be
accomplished with `chain`. Along the same lines, `bichain` allows you to do this
from both [`Rejected`](#rejected) and [`Resolved`](#resolved). `bichain` expects
two unary, `Async` returning functions as its arguments. When invoked on
a [`Rejected`](#rejected) instance, `bichain` will use
the [`Rejected`](#rejected) value and can return either a [`Rejected`](#rejected) or
a [`Resolved`](#resolved). When called on a [`Resolved`](#resolved) instance, it
will behave exactly as [`chain`](#chain) would.

<!-- eslint-disable no-console -->
<!-- eslint-disable no-sequences -->

```javascript
import bichain from 'crocks/pointfree/bichain'

import Async from 'crocks/Async'

import equals from 'crocks/pointfree/equals'
import maybeToAsync from 'crocks/Async/maybeToAsync'
import propSatisfies from 'crocks/predicates/propSatisfies'
import safe from 'crocks/Maybe/safe'
import substitution from 'crocks/combinators/substitution'

const { Rejected, Resolved } = Async

// log :: String -> a -> a
const log = label => x =>
(console.log(`${label}:`, x), x)

const fork = m =>
m.fork(log('rej'), log('res'))

fork(
bichain(Resolved, Rejected, Resolved(42))
)
//=> rej: 42

fork(
bichain(Resolved, Rejected, Rejected(42))
)
//=> res: 42

// fake401 :: Async Response a
const fake401 = Rejected({
status: 'Unauthorized',
statusCode: 401
})

// fake500 :: Async Response a
const fake500 = Rejected({
status: 'Internal Server Error',
statusCode: 500
})

// fake200 :: Async e Response
const fake200 = Resolved({
status: 'OK',
statusCode: 200
})

// allow401 :: Response -> Async e a
const allow401 = substitution(
maybeToAsync,
safe(propSatisfies('statusCode', equals(401)))
)

fork(bichain(allow401, Resolved, fake500))
//=> rej: { status: 'Internal Server Error', statusCode: 500 }

fork(bichain(allow401, Resolved, fake401))
//=> res: { status: 'Unauthorized', statusCode: 401 }

fork(bichain(allow401, Resolved, fake200))
//=> res: { status: 'OK', statusCode: 200 }
```

#### swap

```haskell
Expand Down
103 changes: 100 additions & 3 deletions src/Async/Async.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ test('Async type', t => {

test('Async @@type', t => {
t.equal(Async(unit)['@@type'], Async['@@type'], 'static and instance versions are the same')
t.equal(Async(unit)['@@type'], 'crocks/Async@4', 'returns crocks/Async@4')
t.equal(Async(unit)['@@type'], 'crocks/Async@5', 'returns crocks/Async@5')

t.end()
})
Expand Down Expand Up @@ -465,6 +465,27 @@ test('Async fork settle', t => {
t.end()
})

test('Async cancel bichain cleanup functions - rejected', t => {
const rejCleanUp = sinon.spy()
const forkCleanUp = sinon.spy()

const cancel =
Async.Rejected(0)
.bichain(x => Async(rej => { rej(x); return rejCleanUp }), Async.of)
.fork(unit, unit, forkCleanUp)

cancel()

t.ok(forkCleanUp.calledAfter(rejCleanUp), 'calls the fork cleanup last')

cancel()

t.ok(rejCleanUp.calledOnce, 'calls the Async level cleanup only once')
t.ok(forkCleanUp.calledOnce, 'calls the fork level cleanup only once')

t.end()
})

test('Async cancel chain cleanup functions', t => {
const resCleanUp = sinon.spy()
const rejCleanUp = sinon.spy()
Expand Down Expand Up @@ -1051,7 +1072,7 @@ test('Async alt properties (Alt)', t => {
a.alt(b).alt(c).fork(unit, assocLeft)
a.alt(b.alt(c)).fork(unit, assocRight)

t.same(assocLeft.args[0], assocRight.args[0], 'assosiativity')
t.same(assocLeft.args[0], assocRight.args[0], 'associativity')

const distLeft = sinon.spy()
const distRight = sinon.spy()
Expand Down Expand Up @@ -1224,7 +1245,7 @@ test('Async chain properties (Chain)', t => {
Async((_, res) => res(x)).chain(f).chain(g).fork(unit, aRes)
Async((_, res) => res(x)).chain(y => f(y).chain(g)).fork(unit, bRes)

t.same(aRes.args[0], bRes.args[0], 'assosiativity')
t.same(aRes.args[0], bRes.args[0], 'associativity')

t.end()
})
Expand Down Expand Up @@ -1253,3 +1274,79 @@ test('Async chain properties (Monad)', t => {

t.end()
})

test('Async bichain left errors', t => {
const bichain = bindFunc(Async(unit).bichain)

const err = /Async.bichain: Both arguments must be Async returning functions/
t.throws(bichain(undefined, Async.of), err, 'throws with undefined')
t.throws(bichain(null, Async.of), err, 'throws with null')
t.throws(bichain(0, Async.of), err, 'throws with falsey number')
t.throws(bichain(1, Async.of), err, 'throws with truthy number')
t.throws(bichain('', Async.of), err, 'throws with falsey string')
t.throws(bichain('string', Async.of), err, 'throws with truthy string')
t.throws(bichain(false, Async.of), err, 'throws with false')
t.throws(bichain(true, Async.of), err, 'throws with true')
t.throws(bichain([], Async.of), err, 'throws with an array')
t.throws(bichain({}, Async.of), err, 'throws with an object')

const noAsync = /Async.bichain: Both arguments must be Async returning functions/
t.throws(Async.Rejected(3).bichain(unit, Async.of).fork.bind(null, unit, unit), noAsync, 'throws with a non-Async returning function')

t.doesNotThrow(Async.Rejected(3).bichain(Async.of, Async.of).fork.bind(null, unit, unit), 'allows an Async returning function')

t.end()
})

test('Async bichain right errors', t => {
const bichain = bindFunc(Async(unit).bichain)

const err = /Async.bichain: Both arguments must be Async returning functions/

t.throws(bichain(Async.Rejected, undefined), err, 'throws with undefined')
t.throws(bichain(Async.Rejected, null), err, 'throws with null')
t.throws(bichain(Async.Rejected, 0), err, 'throws with falsey number')
t.throws(bichain(Async.Rejected, 1), err, 'throws with truthy number')
t.throws(bichain(Async.Rejected, ''), err, 'throws with falsey string')
t.throws(bichain(Async.Rejected, 'string'), err, 'throws with truthy string')
t.throws(bichain(Async.Rejected, false), err, 'throws with false')
t.throws(bichain(Async.Rejected, true), err, 'throws with true')
t.throws(bichain(Async.Rejected, []), err, 'throws with an array')
t.throws(bichain(Async.Rejected, {}), err, 'throws with an object')

t.throws(Async.of(3).bichain(Async.Rejected, unit).fork.bind(null, unit, unit), err, 'throws with a non-Async returning function')

t.doesNotThrow(Async.of(3).bichain(Async.Rejected, Async.of).fork.bind(null, unit, unit), 'allows an Async returning function')

t.end()
})

test('Async bichain properties (Bichain)', t => {
t.ok(isFunction(Async(unit).bichain), 'provides a bichain function')

const aRej = sinon.spy()
const bRej = sinon.spy()

const fOfL = x => Async((rej) => rej(x + 2))
const gOfL = x => Async((rej) => rej(x + 10))

const aRes = sinon.spy()
const bRes = sinon.spy()

const fOfR = x => Async((_, res) => res(x * 2))
const gOfR = x => Async((_, res) => res(x * 10))

const x = 12
const y = 7

Async((rej) => rej(x)).bichain(fOfL, Async.of).bichain(gOfL, Async.of).fork(aRej, unit)
Async((rej) => rej(x)).bichain(y => fOfL(y).bichain(gOfL, Async.of), Async.of).fork(bRej, unit)

Async((rej) => rej(y)).bichain(Async.Rejected, fOfR).bichain(Async.Rejected, gOfR).fork(unit, aRes)
Async((rej) => rej(y)).bichain(Async.Rejected, y => fOfR(y).bichain(Async.Rejected, gOfR)).fork(unit, bRes)

t.same(aRej.args[0], bRej.args[0], 'left associativity')
t.same(aRes.args[0], bRes.args[0], 'right associativity')

t.end()
})
32 changes: 31 additions & 1 deletion src/Async/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** @license ISC License (c) copyright 2017 original and current authors */
/** @author Ian Hofmann-Hicks (evil) */

const VERSION = 4
const VERSION = 5

const _implements = require('../core/implements')
const _inspect = require('../core/inspect')
Expand Down Expand Up @@ -322,6 +322,35 @@ function Async(fn) {
}
}

function bichain(l, r) {
const bichainErr = 'Async.bichain: Both arguments must be Async returning functions'

if(!isFunction(l) || !isFunction(r)) {
throw new TypeError(bichainErr)
}

return Async(function(rej, res) {
let cancel = unit
let innerCancel = unit

function setInnerCancel(mapFn) {
return function(x) {
const m = mapFn(x)

if(!isSameType(Async, m)) {
throw new TypeError(bichainErr)
}

innerCancel = m.fork(rej, res)
}
}

cancel = fork(setInnerCancel(l), setInnerCancel(r))

return once(() => innerCancel(cancel()))
})
}

return {
fork, toPromise, inspect,
toString: inspect, type,
Expand All @@ -331,6 +360,7 @@ function Async(fn) {
bimap: bimap('bimap'),
map: map('map'),
chain: chain('chain'),
bichain,
[fl.of]: of,
[fl.alt]: alt(fl.alt),
[fl.bimap]: bimap(fl.bimap),
Expand Down
11 changes: 11 additions & 0 deletions src/core/isBichain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** @license ISC License (c) copyright 2019 original and current authors */
/** @author Dale Francis (dalefrancis88) */

const hasAlg = require('./hasAlg')

// isBichain : a -> Boolean
function isBichain(m) {
return hasAlg('bichain', m)
}

module.exports = isBichain
21 changes: 21 additions & 0 deletions src/pointfree/bichain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/** @license ISC License (c) copyright 2019 original and current authors */
/** @author Dale Francis (dalefrancis88) */

const curry = require('../core/curry')
const isBichain = require('../core/isBichain')
const isFunction = require('../core/isFunction')

// bichain : bichain m => (e -> m c b) -> (a -> m c b) -> m e a -> m c b
function bichain(f, g, m) {
if(!isFunction(f) || !isFunction(g)) {
throw new TypeError('bichain: First two arguments must be Bichain returning functions')
}

if(!isBichain(m)) {
throw new TypeError('bichain: Third argument must be a Bichain')
}

return m.bichain.call(m, f, g)
}

module.exports = curry(bichain)
76 changes: 76 additions & 0 deletions src/pointfree/bichain.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const test = require('tape')
const sinon = require('sinon')
const helpers = require('../test/helpers')

const bindFunc = helpers.bindFunc

const isFunction = require('../core/isFunction')
const unit = require('../core/_unit')

const constant = x => () => x

const mock = x => Object.assign({}, {
bichain: sinon.spy()
}, x)

const bichain = require('./bichain')

test('bichain pointfree', t => {
const m = bindFunc(bichain)
const f = { bichain: unit }

t.ok(isFunction(bichain), 'is a function')

const err = /TypeError: bichain: First two arguments must be Bichain returning functions/

t.throws(m(undefined, unit, f), err, 'throws if first arg is undefined')
t.throws(m(null, unit, f), err, 'throws if first arg is null')
t.throws(m(0, unit, f), err, 'throws if first arg is a falsey number')
t.throws(m(1, unit, f), err, 'throws if first arg is a truthy number')
t.throws(m('', unit, f), err, 'throws if first arg is a falsey string')
t.throws(m('string', unit, f), err, 'throws if first arg is a truthy string')
t.throws(m(false, unit, f), err, 'throws if first arg is false')
t.throws(m(true, unit, f), err, 'throws if first arg is true')
t.throws(m([], unit, f), err, 'throws if first arg is an array')
t.throws(m({}, unit, f), err, 'throws if first arg is an object')

t.throws(m(unit, undefined, f), err, 'throws if second arg is undefined')
t.throws(m(unit, null, f), err, 'throws if second arg is null')
t.throws(m(unit, 0, f), err, 'throws if second arg is a falsey number')
t.throws(m(unit, 1, f), err, 'throws if second arg is a truthy number')
t.throws(m(unit, '', f), err, 'throws if second arg is a falsey string')
t.throws(m(unit, 'string', f), err, 'throws if second arg is a truthy string')
t.throws(m(unit, false, f), err, 'throws if second arg is false')
t.throws(m(unit, true, f), err, 'throws if second arg is true')
t.throws(m(unit, [], f), err, 'throws if second arg is an array')
t.throws(m(unit, {}, f), err, 'throws if second arg is an object')

const last = /bichain: Third argument must be a Bichain/
t.throws(m(unit, unit, undefined), last, 'throws if third arg is undefined')
t.throws(m(unit, unit, null), last, 'throws if third arg is null')
t.throws(m(unit, unit, 0), last, 'throws if third arg is a falsey number')
t.throws(m(unit, unit, 1), last, 'throws if third arg is a truthy number')
t.throws(m(unit, unit, ''), last, 'throws if third arg is a falsey string')
t.throws(m(unit, unit, 'string'), last, 'throws if third arg is a truthy string')
t.throws(m(unit, unit, false), last, 'throws if third arg is false')
t.throws(m(unit, unit, true), last, 'throws if third arg is true')
t.throws(m(unit, unit, {}), last, 'throws if third arg is an object')

t.end()
})

test('bichain with Bichain', t => {
const x = 'result'
const m = mock({ bichain: sinon.spy(constant(x)) })

const f = sinon.spy()
const g = sinon.spy()

const result = bichain(f, g)(m)

t.ok(m.bichain.calledWith(f, g), 'calls bichain on third argument, passing the (2) functions')
t.ok(m.bichain.calledOn(m), 'binds bichain to third argument')
t.equal(result, x, 'returns the result of bichain on third argument')

t.end()
})

0 comments on commit 1d13450

Please sign in to comment.