diff --git a/README.md b/README.md index 37d11fe70..da091e328 100644 --- a/README.md +++ b/README.md @@ -59,23 +59,26 @@ There are (5) classifications of "things" included in this library: ### Crocks The `Crocks` are the heart and soul of this library. This is where you will find all your favorite ADT's you have grown to :heart:. They include gems such as: `Maybe`, `Either` and `IO`, to name a few. The are usually just a simple constructor that takes either a function or value (depending on the type) and will return you a "container" that wraps whatever you passed it. Each container provides a variety of functions that act as the operations you can do on the contained value. There are many types that share the same function names, but what they do from type to type may vary. Every `Crock` provides `type` function on the Constructor and both `inspect` and `type` functions on their Instances. -All `Crocks` are Constructor functions of the given type, with `Writer` being an exception. The `Writer` function takes a `Monoid` that will represent the `log`. Once you provide the `Monoid`, the function will return the `Writer` Constructor for your `Writer` for that specific `Monoid`. +All `Crocks` are Constructor functions of the given type, with `Writer` being an exception. The `Writer` function takes a `Monoid` that will represent the `log`. Once you provide the `Monoid`, the function will return the `Writer` Constructor for your `Writer` using that specific `Monoid`. | Crock | Constructor | Instance | |---|---|---| -| `Arrow` | `empty` | `value`, `runWith`, `concat`, `empty`, `map`, `contramap`, `promap`, `first`, `second` | -| `Const` | -- | `equals`, `value`, `concat`, `map`, `ap`, `chain` | -| `Either` | `Left`, `Right`, `of`| `equals`, `value`, `either`, `swap`, `coalesce`, `map`, `bimap`, `ap`, `of`, `chain`, `sequence`, `traverse` | -| `Identity` | `of` | `equals`, `value`, `map`, `ap`, `of`, `chain`, `sequence`, `traverse` | -| `IO` | `of` | `run`, `map`, `ap`, `of`, `chain` | -| `List` | `empty`, `of` | `equals`, `value`, `head`, `tail`, `cons`, `concat`, `empty`, `reduce`, `filter`, `map`, `ap`, `of`, `chain`, `sequence`, `traverse` | -| `Maybe` | `Nothing`, `Just`, `of` | `equals`, `maybe`, `either`, `option`, `coalesce`, `map`, `ap`, `of`, `chain`, `sequence`, `traverse` | -| `Pair` | `of` | `equals`, `value`, `fst`, `snd`, `merge`, `concat`, `swap`, `map`, `bimap`, `ap`, `of`, `chain` | -| `Reader` | `ask`, `of`| `runWith`, `map`, `ap`, `of`, `chain` | -| `Star` | -- | `runWith`, `map`, `contramap`, `promap` | -| `State` | `get`, `gets`, `put`, `modify` `of`| `runWith`, `execWith`, `evalWith`, `map`, `ap`, `of`, `chain` | -| `Unit` | `empty`, `of` | `equals`, `value`, `concat`, `empty`, `map`, `ap`, `of`, `chain` | -| `Writer`| `of` | `equals`, `value`, `log`, `read`, `map`, `ap`, `of`, `chain` | +| `Arrow` | `empty` | `concat`, `contramap`, `empty`, `first`, `map`, `promap`, `runWith`, `second`, `value` | +| `Const` | -- | `ap`, `chain`, `concat`, `equals`, `map`, `value` | +| `Either` | `Left`, `Right`, `of`| `ap`, `bimap`, `chain`, `coalesce`, `either`, `equals`, `map`, `of`, `sequence`, `swap`, `traverse`, `value` | +| `Identity` | `of` | `ap`, `chain`, `equals`, `map`, `of`, `sequence`, `traverse`, `value` | +| `IO` | `of` | `ap`, `chain`, `map`, `of`, `run` | +| `List` | `empty`, `of` | `ap`, `chain`, `concat`, `cons`, `empty`, `equals`, `filter`, `head`, `map`, `of`, `reduce`, `sequence`, `tail`, `traverse`, `value` | +| `Maybe` | `Nothing`, `Just`, `of` | `ap`, `chain`, `coalesce`, `equals`, `either`, `map`, `maybe`, `of`, `option`, `sequence`, `traverse` | +| `Pair` | `of` | `ap`, `bimap`, `chain`, `concat`, `equals`, `fst`, `map`, `merge`, `of`, `snd`, `swap`, `value` | +| `Pred`[*] | `empty` | `concat`, `contramap`, `empty`, `runWith`, `value` | +| `Reader` | `ask`, `of`| `ap`, `chain`, `map`, `of`, `runWith` | +| `Star` | -- | `contramap`, `map`, `promap`, `runWith` | +| `State` | `get`, `gets`, `modify` `of`, `put`| `ap`, `chain`, `evalWith`, `execWith`, `map`, `of`, `runWith` | +| `Unit` | `empty`, `of` | `ap`, `chain`, `concat`, `empty`, `equals`, `map`, `of`, `value` | +| `Writer`| `of` | `ap`, `chain`, `equals`, `log`, `map`, `of`, `read`, `value` | + +[*] based on [this article](https://medium.com/@drboolean/monoidal-contravariant-functors-are-actually-useful-1032211045c4#.polugsx2a) ### Monoids Each `Monoid` provides a means to represent a binary operation and is usually locked down to a specific type. These are great when you need to combine a list of values down to one value. In this library, any ADT that provides both an `empty` and `concat` function can be used as a `Monoid`. There are a few of the `Crocks` that are also monoidial, so be on the look out for those as well. All `Monoids` work with the point-free functions `mconcat`, `mreduce`, `mconcatMap` and `mreduceMap`. @@ -229,9 +232,9 @@ These functions provide a very clean way to build out very simple functions and | `bimap` | `Either`, `Pair` | | `chain` | `Const`, `Either`, `Identity`, `IO`, `List`, `Maybe`, `Pair`, `Reader`, `State`, `Unit`, `Writer` | | `coalesce` | `Maybe`, `Either` | -| `concat` | `Array`, `String`, `Arrow`, `Const`, `List`, `Pair`, `Unit`, `All`, `Any`, `Assign`, `Max`, `Min`, `Prod`, `Sum` | +| `concat` | `All`, `Any`, `Array`, `Arrow`, `Assign`, `Const`, `List`, `Max`, `Min`, `Pair`, `Pred`, `Prod`, `String`, `Sum`, `Unit` | | `cons` | `Array`, `List` | -| `contramap` | `Arrow`, `Star` | +| `contramap` | `Arrow`, `Pred`, `Star` | | `either` | `Either`, `Maybe` | | `evalWith` | `State` | | `execWith` | `State` | @@ -240,7 +243,7 @@ These functions provide a very clean way to build out very simple functions and | `fst` | `Pair` | | `head` | `Array, List` | | `log` | `Writer` | -| `map` | `Array`, `Function`, `Arrow`, `Const`, `Either`, `Identity`, `IO`, `List`, `Maybe`, `Pair`, `Reader`, `Star`, `State`, `Unit`, `Writer` | +| `map` | `Array`, `Arrow`, `Const`, `Either`, `Function`, `Identity`, `IO`, `List`, `Maybe`, `Pair`, `Reader`, `Star`, `State`, `Unit`, `Writer` | | `maybe` | `Maybe` | | `merge` | `Pair` | | `option` | `Either`, `Maybe` | @@ -248,11 +251,11 @@ These functions provide a very clean way to build out very simple functions and | `read` | `Writer` | | `reduce` | `Array`, `List` | | `run` | `IO` | -| `runWith` | `Arrow`, `Reader`, `Star`, `State` | +| `runWith` | `Arrow`, `Pred`, `Reader`, `Star`, `State` | | `second` | `Arrow` | | `sequence` | `Either`, `Identity`, `List`, `Maybe` | | `snd` | `Pair` | | `swap` | `Pair` | -| `tail` | `Array`, `String`, `List` | +| `tail` | `Array`, `List`, `String` | | `traverse` | `Either`, `Identity`, `List`, `Maybe` | -| `value` | `Arrow`, `Const`, `Either`, `Identity`, `List`, `Pair`, `Unit`, `Writer` | +| `value` | `Arrow`, `Const`, `Either`, `Identity`, `List`, `Pair`, `Pred`, `Unit`, `Writer` | diff --git a/crocks.js b/crocks.js index cc8dee469..582b7a1e3 100644 --- a/crocks.js +++ b/crocks.js @@ -10,6 +10,7 @@ const crocks = { List: require('./crocks/List'), Maybe: require('./crocks/Maybe'), Pair: require('./crocks/Pair'), + Pred: require('./crocks/Pred'), Reader: require('./crocks/Reader'), Star: require('./crocks/Star'), State: require('./crocks/State'), diff --git a/crocks.spec.js b/crocks.spec.js index 4a0a8e8ef..1ee9ea3dd 100644 --- a/crocks.spec.js +++ b/crocks.spec.js @@ -65,6 +65,7 @@ const IO = require('./crocks/IO') const List = require('./crocks/List') const Maybe = require('./crocks/Maybe') const Pair = require('./crocks/Pair') +const Pred = require('./crocks/Pred') const Reader = require('./crocks/Reader') const Star = require('./crocks/Star') const State = require('./crocks/State') @@ -145,6 +146,7 @@ test('entry', t => { t.equal(crocks.List, List, 'provides the List function') t.equal(crocks.Maybe, Maybe, 'provides the Maybe function') t.equal(crocks.Pair, Pair, 'provides the Pair function') + t.equal(crocks.Pred, Pred, 'provides the Pred function') t.equal(crocks.Reader, Reader, 'provides the Reader function') t.equal(crocks.Star, Star, 'provides the Star function') t.equal(crocks.State, State, 'provides the State function') diff --git a/crocks/Arrow.spec.js b/crocks/Arrow.spec.js index 5e1253e58..093a3ad94 100644 --- a/crocks/Arrow.spec.js +++ b/crocks/Arrow.spec.js @@ -85,23 +85,6 @@ test('Arrow runWith', t => { t.end() }) -test('Arrow concat properties (Semigroup)', t => { - const a = Arrow(x => x + 1) - const b = Arrow(x => x * 10) - const c = Arrow(x => x - 5) - - t.ok(isFunction(Arrow(identity).concat), 'is a function') - - const left = a.concat(b).concat(c).runWith - const right = a.concat(b.concat(c)).runWith - const x = 20 - - t.same(left(x), right(x), 'associativity') - t.same(a.concat(b).type(), a.type(), 'returns Semigroup of same type') - - t.end() -}) - test('Arrow concat functionality', t => { const f = x => x + 1 const g = x => x * 0 @@ -133,18 +116,19 @@ test('Arrow concat functionality', t => { t.end() }) -test('Arrow empty properties (Monoid)', t => { - const m = Arrow(x => x + 45) - const x = 32 +test('Arrow concat properties (Semigroup)', t => { + const a = Arrow(x => x + 1) + const b = Arrow(x => x * 10) + const c = Arrow(x => x - 5) - t.ok(isFunction(m.concat), 'provides a concat function') - t.ok(isFunction(m.empty), 'provides a empty function') + t.ok(isFunction(Arrow(identity).concat), 'is a function') - const right = m.concat(m.empty()).runWith - const left = m.empty().concat(m).runWith + const left = a.concat(b).concat(c).runWith + const right = a.concat(b.concat(c)).runWith + const x = 20 - t.same(right(x), m.runWith(x), 'right identity') - t.same(left(x), m.runWith(x), 'left identity') + t.same(left(x), right(x), 'associativity') + t.same(a.concat(b).type(), a.type(), 'returns Semigroup of same type') t.end() }) @@ -160,6 +144,22 @@ test('Arrow empty functionality', t => { t.end() }) +test('Arrow empty properties (Monoid)', t => { + const m = Arrow(x => x + 45) + const x = 32 + + t.ok(isFunction(m.concat), 'provides a concat function') + t.ok(isFunction(m.empty), 'provides a empty function') + + const right = m.concat(m.empty()).runWith + const left = m.empty().concat(m).runWith + + t.same(right(x), m.runWith(x), 'right identity') + t.same(left(x), m.runWith(x), 'left identity') + + t.end() +}) + test('Arrow map errors', t => { const map = bindFunc(Arrow(noop).map) diff --git a/crocks/IO.js b/crocks/IO.js index cceb29a02..51475fb62 100644 --- a/crocks/IO.js +++ b/crocks/IO.js @@ -16,7 +16,7 @@ const _of = x => IO(constant(x)) function IO(run) { - if(!arguments.length || !isFunction(run)) { + if(!isFunction(run)) { throw new TypeError('IO: Must wrap a function') } diff --git a/crocks/Pred.js b/crocks/Pred.js new file mode 100644 index 000000000..4523e8006 --- /dev/null +++ b/crocks/Pred.js @@ -0,0 +1,61 @@ +/** @license ISC License (c) copyright 2016 original and current authors */ +/** @author Ian Hofmann-Hicks (evil) */ + +const isFunction = require('../internal/isFunction') +const isType = require('../internal/isType') + +const _inspect = require('../funcs/inspect') + +const constant = require('../combinators/constant') +const composeB = require('../combinators/composeB') + +const _type = + constant('Pred') + +const _empty = + () => Pred(constant(true)) + +function Pred(runWith) { + if(!isFunction(runWith)) { + throw new TypeError('Pred: Predicate function required') + } + + const type = + _type + + const inspect = + constant(`Pred${_inspect(runWith)}`) + + const empty = + _empty + + const value = + constant(runWith) + + function concat(m) { + if(!(m && isType(type(), m))) { + throw new TypeError('Pred.concat: Pred required') + } + + return Pred(x => runWith(x) && m.runWith(x)) + } + + function contramap(fn) { + if(!isFunction(fn)) { + throw new TypeError('Pred.contramap: Function required') + } + + return Pred(composeB(runWith, fn)) + } + + return { + runWith, inspect, type, + value, empty, concat, + contramap + } +} + +Pred.empty = _empty +Pred.type = _type + +module.exports = Pred diff --git a/crocks/Pred.spec.js b/crocks/Pred.spec.js new file mode 100644 index 000000000..29b881ac4 --- /dev/null +++ b/crocks/Pred.spec.js @@ -0,0 +1,195 @@ +const test = require('tape') +const sinon = require('sinon') +const helpers = require('../test/helpers') + +const bindFunc = helpers.bindFunc +const isFunction = require('../internal/isFunction') +const isObject = require('../internal/isObject') +const noop = helpers.noop + +const constant = require('../combinators/constant') +const identity = require('../combinators/identity') +const composeB = require('../combinators/composeB') + +const Pred = require('./Pred') + +test('Pred', t => { + const p = bindFunc(Pred) + + t.ok(isFunction(Pred), 'is a function') + + t.ok(isFunction(Pred.type), 'provides a type function') + t.ok(isFunction(Pred.empty), 'provides an empty function') + + t.ok(isObject(Pred(noop)), 'returns an object') + + t.throws(Pred, TypeError, 'throws with nothing') + t.throws(p(undefined), TypeError, 'throws with undefined') + t.throws(p(null), TypeError, 'throws with undefined') + t.throws(p(0), TypeError, 'throws with falsey number') + t.throws(p(1), TypeError, 'throws with truthy number') + t.throws(p(''), TypeError, 'throws with falsey string') + t.throws(p('string'), TypeError, 'throws with truthy string') + t.throws(p(false), TypeError, 'throws with false') + t.throws(p(true), TypeError, 'throws with true') + t.throws(p({}), TypeError, 'throws with an object') + t.throws(p([]), TypeError, 'throws with an array') + + t.doesNotThrow(p(noop), 'allows a function') + + t.end() +}) + +test('Pred inspect', t => { + const m = Pred(noop) + + t.ok(isFunction(m.inspect), 'provides an inpsect function') + t.equal(m.inspect(), 'Pred Function', 'returns inspect string') + + t.end() +}) + +test('Pred type', t => { + t.equal(Pred(noop).type(), 'Pred', 'type returns Pred') + t.end() +}) + +test('Pred value', t => { + const f = constant('some Predicate') + const p = Pred(f) + + t.ok(isFunction(p.value), 'is a function') + t.equal(p.value()(), f(), 'provides the wrapped function') + + t.end() +}) + +test('Pred runWith', t => { + const fn = sinon.spy(constant('result')) + const m = Pred(fn) + + const result = m.runWith(false) + + t.ok(fn.called, 'calls the wrapped function') + t.equal(result, fn(),'returns result of the wrapped function' ) + + t.end() +}) + +test('Pred contramap errors', t => { + const cmap = bindFunc(Pred(noop).contramap) + + t.throws(cmap(undefined), TypeError, 'throws with undefined') + t.throws(cmap(null), TypeError, 'throws with null') + t.throws(cmap(0), TypeError, 'throws with falsey number') + t.throws(cmap(1), TypeError, 'throws with truthy number') + t.throws(cmap(''), TypeError, 'throws with falsey string') + t.throws(cmap('string'), TypeError, 'throws with truthy string') + t.throws(cmap(false), TypeError, 'throws with false') + t.throws(cmap(true), TypeError, 'throws with true') + t.throws(cmap([]), TypeError, 'throws with an array') + t.throws(cmap({}), TypeError, 'throws with an object') + + t.doesNotThrow(cmap(noop), 'allows functions') + + t.end() +}) + +test('Pred contramap functionality', t => { + const spy = sinon.spy(identity) + const x = 23 + + const m = Pred(identity).contramap(spy) + + t.equal(m.type(), 'Pred', 'returns a Pred') + t.notOk(spy.called, 'does not call mapping function initially') + + m.runWith(x) + + t.ok(spy.called, 'calls mapping function when ran') + t.equal(m.runWith(x), x, 'returns the result of the resulting composition') + + t.end() +}) + +test('Pred contramap properties (Contra Functor)', t => { + const m = Pred(identity) + + const f = x => x + 17 + const g = x => x * 3 + + const x = 32 + + t.ok(isFunction(m.contramap), 'provides a contramap function') + + t.equal(m.contramap(identity).runWith(x), m.runWith(x), 'identity') + t.equal(m.contramap(composeB(f, g)).runWith(x), m.contramap(f).contramap(g).runWith(x), 'composition') + + t.end() +}) + +test('Pred concat functionality', t => { + const a = Pred(constant(true)) + const b = Pred(constant(false)) + + const notPred = { type: constant('Pred...Not') } + + const cat = bindFunc(a.concat) + + t.throws(cat(undefined), TypeError, 'throws with undefined') + t.throws(cat(null), TypeError, 'throws with null') + t.throws(cat(0), TypeError, 'throws with falsey number') + t.throws(cat(1), TypeError, 'throws with truthy number') + t.throws(cat(''), TypeError, 'throws with falsey string') + t.throws(cat('string'), TypeError, 'throws with truthy string') + t.throws(cat(false), TypeError, 'throws with false') + t.throws(cat(true), TypeError, 'throws with true') + t.throws(cat([]), TypeError, 'throws with an array') + t.throws(cat({}), TypeError, 'throws with an object') + t.throws(cat(notPred), TypeError, 'throws when passed non-Pred') + + t.equal(a.concat(a).runWith(), true, 'true to true reports true') + t.equal(a.concat(b).runWith(), false, 'true to false reports false') + t.equal(b.concat(b).runWith(), false, 'false to false reports false') + + t.end() +}) + +test('Pred concat properties (Semigoup)', t => { + const a = Pred(constant(false)) + const b = Pred(constant(true)) + const c = Pred(constant(false)) + + const left = a.concat(b).concat(c) + const right = a.concat(b.concat(c)) + + t.ok(isFunction(a.concat), 'provides a concat function') + t.equal(left.runWith(), right.runWith(), 'associativity') + t.equal(a.concat(b).type(), a.type(), 'returns a Pred') + + t.end() +}) + +test('Pred empty functionality', t => { + const p = Pred(identity).empty() + + t.equal(p.type(), 'Pred', 'provides a Pred') + t.equal(p.runWith(), true, 'provides a true value') + + t.end() +}) + +test('Pred empty properties (Monoid)', t => { + const m = Pred(constant(true)) + + t.ok(isFunction(m.concat), 'provides a concat function') + t.ok(isFunction(m.empty), 'provides an empty function') + + const right = m.concat(m.empty()) + const left = m.empty().concat(m) + + t.equal(right.runWith(), m.runWith(), 'right identity') + t.equal(left.runWith(), m.runWith(), 'left identity') + + t.end() +}) diff --git a/monoids/All.spec.js b/monoids/All.spec.js index 5b517488d..125025b59 100644 --- a/monoids/All.spec.js +++ b/monoids/All.spec.js @@ -70,21 +70,6 @@ test('All type', t => { t.end() }) -test('All concat properties (Semigoup)', t => { - const a = All(0) - const b = All(true) - const c = All('') - - const left = a.concat(b).concat(c) - const right = a.concat(b.concat(c)) - - t.ok(isFunction(a.concat), 'provides a concat function') - t.equal(left.value(), right.value(), 'associativity') - t.equal(a.concat(b).type(), a.type(), 'returns an All') - - t.end() -}) - test('All concat functionality', t => { const a = All(true) const b = All(false) @@ -112,17 +97,17 @@ test('All concat functionality', t => { t.end() }) -test('All empty properties (Monoid)', t => { - const m = All(3) - - t.ok(isFunction(m.concat), 'provides a concat function') - t.ok(isFunction(m.empty), 'provides an empty function') +test('All concat properties (Semigoup)', t => { + const a = All(0) + const b = All(true) + const c = All('') - const right = m.concat(m.empty()) - const left = m.empty().concat(m) + const left = a.concat(b).concat(c) + const right = a.concat(b.concat(c)) - t.equal(right.value(), m.value(), 'right identity') - t.equal(left.value(), m.value(), 'left identity') + t.ok(isFunction(a.concat), 'provides a concat function') + t.equal(left.value(), right.value(), 'associativity') + t.equal(a.concat(b).type(), a.type(), 'returns an All') t.end() }) @@ -135,3 +120,18 @@ test('All empty functionality', t => { t.end() }) + +test('All empty properties (Monoid)', t => { + const m = All(3) + + t.ok(isFunction(m.concat), 'provides a concat function') + t.ok(isFunction(m.empty), 'provides an empty function') + + const right = m.concat(m.empty()) + const left = m.empty().concat(m) + + t.equal(right.value(), m.value(), 'right identity') + t.equal(left.value(), m.value(), 'left identity') + + t.end() +})