Skip to content

Commit

Permalink
Merge pull request #46 from evilsoft/both-to-arrow-star
Browse files Browse the repository at this point in the history
Add `both` to `Arrow` and `Star` with an additional pointfree function
  • Loading branch information
evilsoft authored Feb 2, 2017
2 parents 0072599 + 9f0b486 commit 0127d9e
Show file tree
Hide file tree
Showing 18 changed files with 240 additions and 23 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ node_modules
dist/

.coveralls.yml
.nyc_output/
.nyc_output
coverage

*.log

Expand Down
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ test
.jshintignore
.jshintrc

coverage
.coveralls.yml
.nyc_output/

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ While `prop` is good for simple, single-level structures, there may come a time
#### `safe : ((a -> Boolean) | Pred) -> a -> Maybe a`
When using a `Maybe`, it is a common practice to lift into a `Just` or a `Nothing` depending on a condition on the value to be lifted. It is so common that it warrants a function, and that function is called `safe`. Provide a predicate (a function that returns a Boolean) and a value to be lifted. The value will be evaluated against the predicate, and will lift it into a `Just` if true and a `Nothing` if false.

#### `safeLift : ((a -> Boolean) | Pred) -> (a -> b) -> Maybe b`
#### `safeLift : ((a -> Boolean) | Pred) -> (a -> b) -> a -> Maybe b`
While `safe` is used to lift a value into a `Maybe`, you can reach for `safeLift` when you want to run a function in the safety of the `Maybe` context. Just like `safe`, you pass it either a `Pred` or a predicate function to determine if you get a `Just` or a `Nothing`, but then instead of a value, you pass it a unary function. `safeLift` will then give you back a new function that will first lift its argument into a `Maybe` and then maps your original function over the result.

#### `tap : (a -> b) -> a -> a`
Expand Down Expand Up @@ -250,6 +250,7 @@ These functions provide a very clean way to build out very simple functions and
|---|:---|
| `ap` | `m a -> m (a -> b) -> m b` |
| `bimap` | `(a -> c) -> (b -> d) -> m a b -> m c d` |
| `both` | `m (a -> b) -> m ((a, a) -> (b, b))` |
| `chain` | `(a -> m b) -> m a -> m b` |
| `coalesce` | `(a -> b) -> m a b -> m a b` |
| `concat` | `m a -> m a -> m a` |
Expand Down Expand Up @@ -285,6 +286,7 @@ These functions provide a very clean way to build out very simple functions and
|---|:---|
| `ap` | `Async`, `Const`, `Either`, `Identity`, `IO`, `List`, `Maybe`, `Pair`, `Reader`, `State`, `Unit`, `Writer` |
| `bimap` | `Async`, `Either`, `Pair` |
| `both` | `Arrow`, `Function`, `Star` |
| `chain` | `Async`, `Const`, `Either`, `Identity`, `IO`, `List`, `Maybe`, `Pair`, `Reader`, `State`, `Unit`, `Writer` |
| `coalesce` | `Async`, `Maybe`, `Either` |
| `concat` | `All`, `Any`, `Array`, `Arrow`, `Assign`, `Const`, `List`, `Max`, `Min`, `Pair`, `Pred`, `Prod`, `Star`, `String`, `Sum`, `Unit` |
Expand Down
1 change: 1 addition & 0 deletions crocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const monoids = {
const pointFree = {
ap: require('./pointfree/ap'),
bimap: require('./pointfree/bimap'),
both: require('./pointfree/both'),
chain: require('./pointfree/chain'),
coalesce: require('./pointfree/coalesce'),
concat: require('./pointfree/concat'),
Expand Down
4 changes: 3 additions & 1 deletion crocks.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const sequence = require('./pointfree/sequence')
const swap = require('./pointfree/swap')
const traverse = require('./pointfree/traverse')

const both = require('./pointfree/both')
const cons = require('./pointfree/cons')
const either = require('./pointfree/either')
const evalWith = require('./pointfree/evalWith')
Expand Down Expand Up @@ -118,6 +119,7 @@ test('entry', t => {
t.equal(crocks.reverseApply, reverseApply, 'provides the T combinator (reverseApply)')
t.equal(crocks.substitution, substitution, 'provides the S combinator (substitution)')

t.equal(crocks.branch, branch, 'provides the branch function')
t.equal(crocks.compose, compose, 'provides the compose function')
t.equal(crocks.curry, curry, 'provides the curry function')
t.equal(crocks.curryN, curryN, 'provides the curryN function')
Expand Down Expand Up @@ -153,7 +155,7 @@ test('entry', t => {
t.equal(crocks.swap, swap, 'provides the swap point-free function')
t.equal(crocks.traverse, traverse, 'provides the traverse point-free function')

t.equal(crocks.branch, branch, 'provides the branch point-free function')
t.equal(crocks.both, both, 'provides the both point-free function')
t.equal(crocks.cons, cons, 'provides the cons point-free function')
t.equal(crocks.either, either, 'provides the either point-free function')
t.equal(crocks.evalWith, evalWith, 'provides the evalWith point-free function')
Expand Down
11 changes: 10 additions & 1 deletion crocks/Arrow.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,19 @@ function Arrow(runWith) {
})
}

function both() {
return Arrow(function(x) {
if(!(x && x.type && x.type() === Pair.type())) {
throw TypeError('Arrow.both: Pair required for inner argument')
}
return x.bimap(runWith, runWith)
})
}

return {
inspect, type, value, runWith,
concat, empty, map, contramap,
promap, first, second
promap, first, second, both
}
}

Expand Down
29 changes: 29 additions & 0 deletions crocks/Arrow.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,32 @@ test('Arrow second', t => {

t.end()
})

test('Arrow both', t => {
t.ok(isFunction(Arrow(noop).both), 'provides a both function')

const m = Arrow(x => x + 1)

const runWith = bindFunc(m.both().runWith)

t.throws(runWith(undefined), TypeError, 'throws with undefined as inner argument')
t.throws(runWith(null), TypeError, 'throws with null as inner argument')
t.throws(runWith(0), TypeError, 'throws with falsey number as inner argument')
t.throws(runWith(1), TypeError, 'throws with truthy number as inner argument')
t.throws(runWith(''), TypeError, 'throws with falsey string as inner argument')
t.throws(runWith('string'), TypeError, 'throws with truthy string as inner argument')
t.throws(runWith(false), TypeError, 'throws with false as inner argument')
t.throws(runWith(true), TypeError, 'throws with true as inner argument')
t.throws(runWith([]), TypeError, 'throws with an array as inner argument')
t.throws(runWith({}), TypeError, 'throws with an object as inner argument')

t.doesNotThrow(runWith(Pair.of(2)), 'does not throw when inner value is a Pair')

const result = m.both().runWith(Pair(10, 10))

t.equal(result.type(), 'Pair', 'returns a Pair')
t.equal(result.fst(), 11, 'applies the function to the first element of a pair')
t.equal(result.snd(), 11, 'applies the function to the snd element of a pair')

t.end()
})
2 changes: 1 addition & 1 deletion crocks/Async.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function fromNode(fn, ctx) {
const args = argsArray(arguments)

return Async((reject, resolve) => {
fn.apply((ctx | null),
fn.apply(ctx,
args.concat(
(err, data) => err ? reject(err) : resolve(data)
)
Expand Down
21 changes: 20 additions & 1 deletion crocks/Star.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const identity = require('../combinators/identity')
const compose = require('../helpers/compose')
const constant = require('../combinators/constant')

const sequence = require('../pointfree/sequence')

const Pair = require('./Pair')

const _type =
Expand Down Expand Up @@ -124,9 +126,26 @@ function Star(runWith) {
})
}

function both() {
return Star(function(x) {
if(!isType(Pair.type(), x)) {
throw TypeError('Star.both: Pair required for computation input')
}

const p = x.bimap(runWith, runWith)
const m = p.fst()

if(!isMonad(m)) {
throw new TypeError('Star.both: Computaion must return a Monad')
}

return sequence(m.of, p.value()).map(x => Pair(x[0], x[1]))
})
}

return {
inspect, type, runWith, concat, map,
contramap, promap, first, second
contramap, promap, first, second, both
}
}

Expand Down
43 changes: 43 additions & 0 deletions crocks/Star.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,3 +438,46 @@ test('Star second', t => {

t.end()
})

test('Star both', t => {
t.ok(isFunction(Star(noop).both), 'provides a both function')

const m = Star(x => MockCrock(x + 1))

const runWith = bindFunc(m.both().runWith)

t.throws(runWith(undefined), TypeError, 'throws with undefined as input')
t.throws(runWith(null), TypeError, 'throws with null as input')
t.throws(runWith(0), TypeError, 'throws with falsey number as input')
t.throws(runWith(1), TypeError, 'throws with truthy number as input')
t.throws(runWith(''), TypeError, 'throws with falsey string as input')
t.throws(runWith('string'), TypeError, 'throws with truthy string as input')
t.throws(runWith(false), TypeError, 'throws with false as input')
t.throws(runWith(true), TypeError, 'throws with true as input')
t.throws(runWith([]), TypeError, 'throws with an array as input')
t.throws(runWith({}), TypeError, 'throws with an object as input')

t.doesNotThrow(runWith(Pair.of(2)), 'does not throw when inner value is a Pair')

const notValid = bindFunc(x => Star(_ => x).both().runWith(Pair(2, 3)))

t.throws(notValid(undefined), TypeError, 'throws when computation returns undefined')
t.throws(notValid(null), TypeError, 'throws when computation returns null')
t.throws(notValid(0), TypeError, 'throws when computation returns falsey number')
t.throws(notValid(1), TypeError, 'throws when computation returns truthy number')
t.throws(notValid(''), TypeError, 'throws when computation returns falsey string')
t.throws(notValid('string'), TypeError, 'throws when computation returns truthy string')
t.throws(notValid(false), TypeError, 'throws when computation returns false')
t.throws(notValid(true), TypeError, 'throws when computation returns true')
t.throws(notValid({}), TypeError, 'throws an when computation returns object')

t.doesNotThrow(notValid(MockCrock.of(2)), 'does not throw when computation returns a Functor')

const result = m.both().runWith(Pair(10, 10)).value()

t.equal(result.type(), 'Pair', 'returns a Pair')
t.equal(result.fst(), 11, 'applies the function to the first element of a pair')
t.equal(result.snd(), 11, 'applies the function to the snd element of a pair')

t.end()
})
2 changes: 1 addition & 1 deletion helpers/safeLift.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const isType = require('../internal/isType')

const Pred = require('../crocks/Pred')

// safeLift : ((a -> Boolean) | Pred) -> (a -> b) -> Maybe b
// safeLift : ((a -> Boolean) | Pred) -> (a -> b) -> a -> Maybe b
function safeLift(pred, fn) {
if(!(isFunction(pred) || isType(Pred.type(), pred))) {
throw new TypeError('safeLift: Pred or predicate function required for first argument')
Expand Down
12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@
"url": "https://github.com/evilsoft/crocks/issues"
},
"homepage": "https://github.com/evilsoft/crocks#readme",
"nyc" : {
"check-coverage": true,
"lines": 100,
"statements": 100,
"functions": 100,
"branches": 100,
"exclude": [
"dist/",
"test/",
"**/*.spec.js"
]
},
"devDependencies": {
"babel-core": "^6.10.4",
"babel-loader": "^6.2.4",
Expand Down
26 changes: 26 additions & 0 deletions pointfree/both.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** @license ISC License (c) copyright 2017 original and current authors */
/** @author Ian Hofmann-Hicks (evil) */

const isFunction = require('../predicates/isFunction')
const isType = require('../internal/isType')
const identity = require('../combinators/identity')

const Pair = require('../crocks/Pair')

function both(m) {
if(isFunction(m)) {
return function(x) {
if(!(x && isType(Pair.type(), x))) {
throw new TypeError('both: Pair required as input')
}

return x.bimap(m, m)
}
} else if(m && isFunction(m.both)) {
return m.both()
} else {
throw new TypeError('both: Arrow, Function or Star required')
}
}

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

const bindFunc = helpers.bindFunc
const noop = helpers.noop

const isFunction = require('../predicates/isFunction')

const constant = require('../combinators/constant')

const Pair = require('../crocks/Pair')

const both = require('./both')

test('both pointfree', t => {
const f = bindFunc(both)

const x = 'result'
const m = { both: sinon.spy(constant(x)) }

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

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

t.doesNotThrow(f(m), 'allows an Arrow')
t.doesNotThrow(f(noop), 'allows a function')

t.end()
})

test('both with Arrow or Star', t => {
const x = 'result'
const m = { both: sinon.spy(constant(x)) }
const res = both(m)

t.ok(m.both.called, 'calls both on Arrow or Star')
t.equal(res, x, 'returns the result of both on Arrow or Star')

t.end()
})

test('both with Function', t => {
const f = both(x => x + 1)
const g = bindFunc(f)

const res = f(Pair(3, 3))

t.throws(g(undefined), TypeError, 'throws when wrapped function called with undefined')
t.throws(g(null), TypeError, 'throws when wrapped function called with null')
t.throws(g(0), TypeError, 'throws when wrapped function called with falsey number')
t.throws(g(1), TypeError, 'throws when wrapped function called with truthy number')
t.throws(g(''), TypeError, 'throws when wrapped function called with falsey string')
t.throws(g('string'), TypeError, 'throws when wrapped function called with truthy string')
t.throws(g(false), TypeError, 'throws when wrapped function called with false')
t.throws(g(true), TypeError, 'throws when wrapped function called with true')
t.throws(g({}), TypeError, 'throws when wrapped function called with an Object')
t.throws(g([]), TypeError, 'throws when wrapped function called with an Array')
t.throws(g(noop), TypeError, 'throws when wrapped function called with a function')

t.equal(res.fst(), 4, 'Applies function to `fst` of the Pair')
t.equal(res.snd(), 4, 'Applies function to `snd` of the Pair')

t.end()
})
7 changes: 4 additions & 3 deletions pointfree/first.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ const isFunction = require('../predicates/isFunction')
const isType = require('../internal/isType')
const identity = require('../combinators/identity')

const Pair = require('../crocks/Pair')

function first(m) {
if(isFunction(m)) {
return function(x) {
if(!(x && isType('Pair', x))) {
if(!(x && isType(Pair.type(), x))) {
throw new TypeError('first: Pair required as input')
}

Expand All @@ -17,9 +19,8 @@ function first(m) {
} else if(m && isFunction(m.first)) {
return m.first()
} else {
throw new TypeError('first: Arrow or Function required')
throw new TypeError('first: Arrow, Function or Star required')
}

}

module.exports = first
Loading

0 comments on commit 0127d9e

Please sign in to comment.