From 20b526c6e2e64ccffbdef5c9c6119f0a45b52b56 Mon Sep 17 00:00:00 2001 From: Ian Hofmann-Hicks Date: Fri, 22 Dec 2017 09:34:09 -0800 Subject: [PATCH] Add the `Equiv` datatype (#161) * Add Equiv type * add pointfree compareWith, Equiv docs, and curry the compareWith * pr feedback, fix example --- README.md | 2 + src/Equiv/Equiv.spec.js | 255 ++++++++++++++++++++++ src/Equiv/README.md | 347 ++++++++++++++++++++++++++++++ src/Equiv/index.js | 67 ++++++ src/Pred/Pred.spec.js | 69 +++--- src/core/types.js | 2 +- src/index.js | 2 + src/index.spec.js | 4 + src/pointfree/compareWith.js | 15 ++ src/pointfree/compareWith.spec.js | 40 ++++ src/pointfree/runWith.js | 2 +- src/pointfree/runWith.spec.js | 23 +- 12 files changed, 783 insertions(+), 45 deletions(-) create mode 100644 src/Equiv/Equiv.spec.js create mode 100644 src/Equiv/README.md create mode 100644 src/Equiv/index.js create mode 100644 src/pointfree/compareWith.js create mode 100644 src/pointfree/compareWith.spec.js diff --git a/README.md b/README.md index 652e7310c..8a2440717 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ will return the `Writer` Constructor for your `Writer` using that specific | `Async` | `Rejected`, `Resolved`, `all`, `fromNode`, `fromPromise`, `of` | `alt`, `ap`, `bimap`, `chain`, `coalesce`, `fork`, `map`, `of`, `swap`, `toPromise` | | `Const` | -- | `ap`, `chain`, `concat`, `equals`, `map`, `valueOf` | | `Either` | `Left`, `Right`, `of`| `alt`, `ap`, `bimap`, `chain`, `coalesce`, `concat`, `either`, `equals`, `map`, `of`, `sequence`, `swap`, `traverse` | +| `Equiv` | `empty` | `concat`, `contramap`, `empty`, `compareWith`, `valueOf` | | `Identity` | `of` | `ap`, `chain`, `concat`, `equals`, `map`, `of`, `sequence`, `traverse`, `valueOf` | | `IO` | `of` | `ap`, `chain`, `map`, `of`, `run` | | `List` | `empty`, `fromArray`, `of` | `ap`, `chain`, `concat`, `cons`, `empty`, `equals`, `filter`, `head`, `map`, `of`, `reduce`, `reject`, `sequence`, `tail`, `toArray`, `traverse`, `valueOf` | @@ -1252,6 +1253,7 @@ accepted Datatype): | `both` | `m (a -> b) -> m (Pair a a -> Pair b b)` | `crocks/pointfree` | | `chain` | `(a -> m b) -> m a -> m b` | `crocks/pointfree` | | `coalesce` | `(a -> c) -> (b -> c) -> m a b -> m _ c` | `crocks/pointfree` | +| `compareWith` | `a -> a -> m a -> Boolean` | `crocks/pointfree` | | `concat` | `m a -> m a -> m a` | `crocks/pointfree` | | `cons` | `a -> m a -> m a` | `crocks/pointfree` | | `contramap` | `(b -> a) -> m a -> m b` | `crocks/pointfree` | diff --git a/src/Equiv/Equiv.spec.js b/src/Equiv/Equiv.spec.js new file mode 100644 index 000000000..deeea8b14 --- /dev/null +++ b/src/Equiv/Equiv.spec.js @@ -0,0 +1,255 @@ +const test = require('tape') +const sinon = require('sinon') +const helpers = require('../test/helpers') + +const bindFunc = helpers.bindFunc + +const compose = require('../core/compose') +const isFunction = require('../core/isFunction') +const isObject = require('../core/isObject') +const unit = require('../core/_unit') + +const constant = x => () => x +const identity = x => x + +const isSame = + (x, y) => x === y + +const Equiv = require('.') + +test('Equiv', t => { + const e = bindFunc(Equiv) + + t.ok(isFunction(Equiv), 'is a function') + + t.ok(isFunction(Equiv.type), 'provides a type function') + t.ok(isFunction(Equiv.empty), 'provides an empty function') + + t.ok(isObject(Equiv(isSame)), 'returns an object') + + const err = /Equiv: Comparison function required/ + t.throws(Equiv, err, 'throws with nothing') + t.throws(e(undefined), err, 'throws with undefined') + t.throws(e(null), err, 'throws with undefined') + t.throws(e(0), err, 'throws with falsey number') + t.throws(e(1), err, 'throws with truthy number') + t.throws(e(''), err, 'throws with falsey string') + t.throws(e('string'), err, 'throws with truthy string') + t.throws(e(false), err, 'throws with false') + t.throws(e(true), err, 'throws with true') + t.throws(e({}), err, 'throws with an object') + t.throws(e([]), err, 'throws with an array') + + t.doesNotThrow(e(unit), 'allows a function') + + t.end() +}) + +test('Equiv @@implements', t => { + const f = Equiv['@@implements'] + + t.equal(f('concat'), true, 'implements concat func') + t.equal(f('contramap'), true, 'implements contramap func') + t.equal(f('empty'), true, 'implements empty func') + + t.end() +}) + +test('Equiv inspect', t => { + const m = Equiv(isSame) + + t.ok(isFunction(m.inspect), 'provides an inpsect function') + t.equal(m.inspect(), 'Equiv Function', 'returns inspect string') + + t.end() +}) + +test('Equiv type', t => { + t.equal(Equiv(isSame).type(), 'Equiv', 'type returns Equiv') + t.equal(Equiv(isSame).type(), Equiv.type(), 'constructor and instance return same value') + + t.end() +}) + +test('Equiv compareWith', t => { + const fn = sinon.spy(isSame) + const m = Equiv(fn) + + const nonBoolEquiv = + x => Equiv(constant(x)).compareWith(1, 1) + + t.equals(nonBoolEquiv(undefined), false, 'returns false when wrapped function returns undefined') + t.equals(nonBoolEquiv(null), false, 'returns false when wrapped function returns null') + t.equals(nonBoolEquiv(0), false, 'returns false when wrapped function returns falsey number') + t.equals(nonBoolEquiv(1), true, 'returns true when wrapped function returns truthy number') + t.equals(nonBoolEquiv(''), false, 'returns false when wrapped function returns falsey string') + t.equals(nonBoolEquiv('string'), true, 'returns true when wrapped function returns truthy string') + t.equals(nonBoolEquiv(false), false, 'returns false when wrapped function returns false') + t.equals(nonBoolEquiv(true), true, 'returns true when wrapped function returns true') + t.equals(nonBoolEquiv({}), true, 'returns true when wrapped function returns an object') + t.equals(nonBoolEquiv([]), true, 'returns true when wrapped function returns an array') + t.equals(nonBoolEquiv(unit), true, 'returns true when wrapped function returns a function') + + const result = m.compareWith(false, false) + + t.ok(fn.called, 'calls the wrapped function') + t.equal(result, !!fn(false, false),'returns Boolean equiv result of the wrapped function' ) + + t.end() +}) + +test('Equiv valueOf', t => { + const e = Equiv(isSame) + + t.ok(isFunction(e.valueOf), 'is a function') + t.equals(e.valueOf()(4, 5), !!isSame(4, 5), 'returns a coerced to Boolean version of the function') + + t.end() +}) + +test('Equiv contramap errors', t => { + const cmap = bindFunc(Equiv(isSame).contramap) + + const err = /Equiv.contramap: Function required/ + t.throws(cmap(undefined), err, 'throws with undefined') + t.throws(cmap(null), err, 'throws with null') + t.throws(cmap(0), err, 'throws with falsey number') + t.throws(cmap(1), err, 'throws with truthy number') + t.throws(cmap(''), err, 'throws with falsey string') + t.throws(cmap('string'), err, 'throws with truthy string') + t.throws(cmap(false), err, 'throws with false') + t.throws(cmap(true), err, 'throws with true') + t.throws(cmap([]), err, 'throws with an array') + t.throws(cmap({}), err, 'throws with an object') + + t.doesNotThrow(cmap(unit), 'allows functions') + + t.end() +}) + +test('Equiv contramap functionality', t => { + const spy = sinon.spy(identity) + + const x = 23 + const y = 100 + + const m = Equiv(isSame).contramap(spy) + + t.equal(m.type(), 'Equiv', 'returns an Equiv') + t.notOk(spy.called, 'does not call mapping function initially') + + m.compareWith(x, y) + + t.ok(spy.called, 'calls mapping function when ran') + + t.equal( + m.compareWith(x, y), + isSame(x, y), + 'returns the Boolean equiv result of the resulting composition' + ) + + t.end() +}) + +test('Equiv contramap properties (Contra Functor)', t => { + const m = Equiv(isSame) + + const f = x => x + 17 + const g = x => x * 3 + + const x = 32 + const y = 23 + + t.ok(isFunction(m.contramap), 'provides a contramap function') + + t.equal( + m.contramap(identity).compareWith(x, y), + m.compareWith(x, y), + 'identity' + ) + + t.equal( + m.contramap(compose(f, g)).compareWith(x, y), + m.contramap(f).contramap(g).compareWith(x, y), + 'composition' + ) + + t.end() +}) + +test('Equiv concat functionality', t => { + const a = Equiv(constant(true)) + const b = Equiv(constant(false)) + + const notEquiv = { type: constant('Equiv...Not') } + + const cat = bindFunc(a.concat) + + const err = /Equiv.concat: Equiv required/ + t.throws(cat(undefined), err, 'throws with undefined') + t.throws(cat(null), err, 'throws with null') + t.throws(cat(0), err, 'throws with falsey number') + t.throws(cat(1), err, 'throws with truthy number') + t.throws(cat(''), err, 'throws with falsey string') + t.throws(cat('string'), err, 'throws with truthy string') + t.throws(cat(false), err, 'throws with false') + t.throws(cat(true), err, 'throws with true') + t.throws(cat([]), err, 'throws with an array') + t.throws(cat({}), err, 'throws with an object') + t.throws(cat(notEquiv), err, 'throws when passed non-Equiv') + + t.equal(a.concat(a).compareWith(1, 1), true, 'true to true reports true') + t.equal(a.concat(b).compareWith(1, 1), false, 'true to false reports false') + t.equal(b.concat(b).compareWith(1, 1), false, 'false to false reports false') + + t.end() +}) + +test('Equiv concat properties (Semigroup)', t => { + const a = Equiv(constant(false)) + const b = Equiv(constant(true)) + const c = Equiv(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.compareWith('', ''), right.compareWith('', ''), 'associativity') + + t.equal(a.concat(b).type(), a.type(), 'returns an Equiv') + + t.end() +}) + +test('Equiv empty functionality', t => { + const e = Equiv(identity).empty() + + t.equal(e.type(), 'Equiv', 'provides an Equiv') + t.equal(e.compareWith('a', 'A'), true, 'provides a true value') + + t.end() +}) + +test('Equiv empty properties (Monoid)', t => { + const m = Equiv(isSame) + + 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.compareWith(34, 45), + m.compareWith(34, 45), + 'right identity' + ) + + t.equal( + left.compareWith(10, 10), + m.compareWith(10, 10), + 'left identity' + ) + + t.end() +}) diff --git a/src/Equiv/README.md b/src/Equiv/README.md new file mode 100644 index 000000000..20c67c9e1 --- /dev/null +++ b/src/Equiv/README.md @@ -0,0 +1,347 @@ +# Equiv +```haskell +Equiv a a Boolean +``` + +Defined as a Monoidal Contravariant datatype, `Equiv` can be used to test +equivalence between (2) values of a given type. It does this by wrapping a +binary equivalence function of the form `(a, a) -> Boolean`. Most of the time +strict equality is used, but other functions of the required form can provide +some powerful results. + +While the far right parameter is always fixed to `Boolean` it cannot be +Covariant, but is Contravariant allowing both inputs to vary in their type. +`Equiv` is also a `Monoid` and will concat the results of (2) `Equiv`s under +logical conjunction, with it's empty value always returning `true`. + +As `Equiv` wraps a function, it is lazy and a given instance will not produce +a result until both arguments are satisfied. A given instance can be run by +calling the method [`compareWith`](#comparewith), providing both values for +comparison. + +```js +const Equiv = require('crocks/Equiv') +const equals = require('crocks/pointfree/equals') + +// toString :: a -> String +const toString = + x => x.toString() + +// length :: a -> Number +const length = x => + x && x.length ? x.length : 0 + +// eq :: Equiv a a +const eq = + Equiv(equals) + +eq.contramap(toString) + .compareWith('123', 123) +//=> true + +eq.contramap(length) + .compareWith([ 1, 2, 3 ], [ 'a', 'b' ]) +//=> false +``` + +## Implements +`Semigroup`, `Monoid`, `Contravariant` + +## Constructor Methods + +### empty +```haskell +Equiv.empty :: () -> Equiv a a +``` + +`empty` provides the identity for the `Monoid` in that when the value it +provides is `concat`ed to any other value, it will return the other value. In +the case of `Equiv` the result of `empty` is an `Equiv` that will always return +`true`. `empty` is available on both the Constructor and the Instance for +convenience. + +```js +const Equiv = require('crocks/Equiv') +const equals = require('crocks/pointfree/equals') + +const eq = + Equiv(equals) + +const empty = + Equiv.empty() + +eq + .concat(empty) + .compareWith({ a: 32 }, { a: 32 }) +//=> true + +empty + .concat(eq) + .compareWith({ a: 32 }, { a: 32 }) +//=> true + +empty + .concat(eq) + .compareWith({ a: 32, b: 19 }, { a: 32 }) +//=> false +``` + +### type +```haskell +Equiv.type :: () -> String +``` + +`type` provides a string representation of the type name for a given type in +`crocks`. While it is used mostly internally for law validation, it can be +useful to the end user for debugging and building out custom types based on the +standard `crocks` types. While type comparisons can easily be done manually by +calling `type` on a given type, using the `isSameType` function hides much of +the boilerplate. `type` is available on both the Constructor and the Instance +for convenience. + +```js +const Endo = require('crocks/Endo') +const equals = require('crocks/pointfree/equals') +const isSameType = require('crocks/predicates/isSameType') + +Equiv.type() //=> "Equiv" + +isSameType(Equiv, Equiv(equals)) //=> true +isSameType(Equiv, Equiv) //=> true +isSameType(Equiv, Endo(x => x * 2)) //=> false +isSameType(Equiv(equals), Endo) //=> false +``` + +## Instance Methods + +### concat +```haskell +Equiv a a ~> Equiv a a -> Equiv a a +``` + +`concat` is used to combine (2) `Semigroup`s of the same type under an operation +specified by the `Semigroup`. In the case of `Equiv`, the results of both +`Equiv`s are combined under logical conjunction. + +```js +const Equiv = require('crocks/Equiv') +const compareWith = require('crocks/pointfree/compareWith') +const equals = require('crocks/pointfree/equals') +const isSameType = require('crocks/predicates/isSameType') +const propOr = require('crocks/helpers/propOr') + +// objLength :: Object -> Number +const objLength = + x => Object.keys(x).length + +// eq :: Equiv a a +const eq = + Equiv(equals) + +// sameType :: Equiv a a +const sameType = + Equiv(isSameType) + +// sameType :: Equiv Object Object +const length = + eq.contramap(objLength) + +// sameType :: Equiv a a +const sameTypeProp = key => + sameType.contramap(propOr(null, key)) + +// run :: Equiv Object Object +const run = compareWith( + { a: 19, b: 'string' }, + { a: 32, c: false } +) + +run(length) +//=> true + +run(sameTypeProp('a')) +//=> true + +run(sameTypeProp('b')) +//=> false + +run( + sameTypeProp('a') + .concat(length) +) +// true + +run( + sameTypeProp('b') + .concat(length) +) +// false +``` + +### contramap +```haskell +Equiv a a ~> (b -> a) -> Equiv b b +``` + +The far right parameter of `Equiv` fixed to `Boolean` which means we cannot map +the value as expected. However the left two parameters can vary, although they +must vary in the same manner. + +This is where `contramap` comes into play as it can be used to adapt an `Equiv` +of a given type to accept a different type or modify the value. Provide it a +function that has a return type that matches the input types of the `Equiv`. +This will return a new `Equiv` matching the input type of the provided +function. + +```js +const Equiv = require('crocks/Equiv') +const equals = require('crocks/pointfree/equals') + +// length :: String -> Number +const length = + x => x.length + +// eq :: Equiv a a +const eq = + Equiv(equals) + +// sameLength :: Equiv String String +const sameLength = + eq.contramap(length) + +// sameAmplitude :: Equiv Float Float +const sameAmplitude = + eq.contramap(Math.abs) + +sameAmplitude + .compareWith(-0.5011, 0.5011) +//=> true + +sameAmplitude + .compareWith(-0.755, 0.8023) +//=> false + +sameLength + .compareWith('aBcD', '1234') +//=> true + +sameLength + .compareWith('AB', 'ABC') +//=> false +``` + +### valueOf +```haskell +Equiv a a ~> () -> a -> a -> Boolean +``` + +`valueOf` is used on all `crocks` `Monoid`s as a means of extraction. While the +extraction is available, types that implement `valueOf` are not necessarily a +`Comonad`. This function is used primarily for convenience for some of the +helper functions that ship with `crocks`. Calling `valueOf` on an `Equiv` +instance will result in the underlying curried equivalence function. + +```js +const Equiv = require('crocks/Equiv') +const compose = require('crocks/helpers/compose') +const equals = require('crocks/pointfree/equals') +const propOr = require('crocks/helpers/propOr') + +// toLower :: String -> String +const toLower = + x => x.toLowerCase() + +// length :: String -> String +const length = + x => x.length + +// lowerName :: Object -> String +const lowerName = + compose(toLower, propOr('', 'name')) + +// itemsLen :: Object -> Number +const itemsLen = + compose(length, propOr('', 'items')) + +// eq :: Equiv a a +const eq = + Equiv(equals) + +// checkName :: Equiv Object Object +const checkName = + eq.contramap(lowerName) + +// checkName :: Equiv Object Object +const checkItems = + eq.contramap(itemsLen) + +// test :: Object -> Object -> Boolean +const test = + checkName + .concat(checkItems) + .valueOf() + +test( + { name: 'Bob', items: [ 1, 2, 4 ] }, + { name: 'bOb', items: [ 9, 12, 9 ] } +) +//=> true +``` + +### compareWith +```haskell +Equiv a a ~> a -> a -> Boolean +``` + +As `Equiv` wraps a function, it needs a means to be run with two values for +comparison. Instances provide a curried method called `compareWith` that takes +two values for comparison and will run them through the equivalence function, +returning the resulting `Boolean`. + +Due to the laziness of this type, complicated comparisons can be built out from +combining and mapping smaller, simpler units of equivalence comparison. + +```js +const Equiv = require('crocks/Equiv') + +// both :: Equiv Boolean Boolean +const both = + Equiv((x, y) => x && y) + +// isEven :: Number -> Boolean +const isEven = + x => x % 2 === 0 + +// isBig :: Number -> Boolean +const isBig = + x => x > 10 + +// bothEven :: Equiv Number Number +const bothEven = + both.contramap(isEven) + +// bothBig :: Equiv Number Number +const bothBig = + both.contramap(isBig) + +bothEven + .compareWith(12, 20) +//=> true + +bothEven + .compareWith(17, 20) +//=> false + +bothBig + .compareWith(17)(20) +//=> true + +bothBig + .compareWith(7)(20) +//=> false + +bothBig + .concat(bothEven) + .compareWith(8)(54) +//=> false +``` diff --git a/src/Equiv/index.js b/src/Equiv/index.js new file mode 100644 index 000000000..0b476376f --- /dev/null +++ b/src/Equiv/index.js @@ -0,0 +1,67 @@ +/** @license ISC License (c) copyright 2017 original and current authors */ +/** @author Ian Hofmann-Hicks (evil) */ + +const _implements = require('../core/implements') +const _inspect = require('../core/inspect') + +const curry = require('../core/curry') +const isFunction = require('../core/isFunction') +const isSameType = require('../core/isSameType') + +const type = require('../core/types').type('Equiv') + +const _empty = + () => Equiv(() => true) + +function Equiv(compare) { + if(!isFunction(compare)) { + throw new TypeError('Equiv: Comparison function required') + } + + const compareWith = curry( + (x, y) => !!compare(x, y) + ) + + const inspect = + () => `Equiv${_inspect(compare)}` + + const empty = + _empty + + const valueOf = + () => compareWith + + function contramap(fn) { + if(!isFunction(fn)) { + throw new TypeError('Equiv.contramap: Function required') + } + + return Equiv( + (x, y) => compareWith(fn(x), fn(y)) + ) + } + + function concat(m) { + if(!isSameType(Equiv, m)) { + throw new TypeError('Equiv.concat: Equiv required') + } + + return Equiv((x, y) => + compareWith(x, y) && m.compareWith(x, y) + ) + } + + return { + inspect, type, compareWith, valueOf, + contramap, concat, empty + } +} + +Equiv.type = type +Equiv.empty = _empty + +Equiv['@@implements'] = _implements( + [ 'concat', 'contramap', 'empty' ] +) + +module.exports = Equiv diff --git a/src/Pred/Pred.spec.js b/src/Pred/Pred.spec.js index f8fc3db07..86227d9c4 100644 --- a/src/Pred/Pred.spec.js +++ b/src/Pred/Pred.spec.js @@ -24,17 +24,18 @@ test('Pred', t => { t.ok(isObject(Pred(unit)), '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') + const err = /Pred: Predicate function required/ + t.throws(Pred, err, 'throws with nothing') + t.throws(p(undefined), err, 'throws with undefined') + t.throws(p(null), err, 'throws with undefined') + t.throws(p(0), err, 'throws with falsey number') + t.throws(p(1), err, 'throws with truthy number') + t.throws(p(''), err, 'throws with falsey string') + t.throws(p('string'), err, 'throws with truthy string') + t.throws(p(false), err, 'throws with false') + t.throws(p(true), err, 'throws with true') + t.throws(p({}), err, 'throws with an object') + t.throws(p([]), err, 'throws with an array') t.doesNotThrow(p(unit), 'allows a function') @@ -62,6 +63,8 @@ test('Pred inspect', t => { test('Pred type', t => { t.equal(Pred(unit).type(), 'Pred', 'type returns Pred') + t.equal(Pred(unit).type(), Pred.type(), 'constructor and instance return same value') + t.end() }) @@ -105,16 +108,17 @@ test('Pred runWith', t => { test('Pred contramap errors', t => { const cmap = bindFunc(Pred(unit).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') + const err = /Pred.contramap: Function required/ + t.throws(cmap(undefined), err, 'throws with undefined') + t.throws(cmap(null), err, 'throws with null') + t.throws(cmap(0), err, 'throws with falsey number') + t.throws(cmap(1), err, 'throws with truthy number') + t.throws(cmap(''), err, 'throws with falsey string') + t.throws(cmap('string'), err, 'throws with truthy string') + t.throws(cmap(false), err, 'throws with false') + t.throws(cmap(true), err, 'throws with true') + t.throws(cmap([]), err, 'throws with an array') + t.throws(cmap({}), err, 'throws with an object') t.doesNotThrow(cmap(unit), 'allows functions') @@ -162,17 +166,18 @@ test('Pred concat functionality', t => { 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') + const err = /Pred.concat: Pred required/ + t.throws(cat(undefined), err, 'throws with undefined') + t.throws(cat(null), err, 'throws with null') + t.throws(cat(0), err, 'throws with falsey number') + t.throws(cat(1), err, 'throws with truthy number') + t.throws(cat(''), err, 'throws with falsey string') + t.throws(cat('string'), err, 'throws with truthy string') + t.throws(cat(false), err, 'throws with false') + t.throws(cat(true), err, 'throws with true') + t.throws(cat([]), err, 'throws with an array') + t.throws(cat({}), err, 'throws with an object') + t.throws(cat(notPred), err, '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') diff --git a/src/core/types.js b/src/core/types.js index 28a11f252..48ac1e2dc 100644 --- a/src/core/types.js +++ b/src/core/types.js @@ -11,6 +11,7 @@ const _types = { 'Const': () => 'Const', 'Either': () => 'Either', 'Endo': () => 'Endo', + 'Equiv': () => 'Equiv', 'First': () => 'First', 'Identity': () => 'Identity', 'IO': () => 'IO', @@ -31,7 +32,6 @@ const _types = { 'Writer': () => 'Writer', } - const type = type => _types[type] || _types['unk'] diff --git a/src/index.js b/src/index.js index a5e127ff5..d91f8be6c 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,7 @@ const crocks = { Async: require('./Async'), Const: require('./Const'), Either: require('./Either'), + Equiv: require('./Equiv'), Identity: require('./Identity'), IO: require('./IO'), List: require('./List'), @@ -107,6 +108,7 @@ const pointfree = { both: require('./pointfree/both'), chain: require('./pointfree/chain'), coalesce: require('./pointfree/coalesce'), + compareWith: require('./pointfree/compareWith'), concat: require('./pointfree/concat'), cons: require('./pointfree/cons'), contramap: require('./pointfree/contramap'), diff --git a/src/index.spec.js b/src/index.spec.js index 8ba27083c..ef8d19e12 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -15,6 +15,7 @@ const Arrow = require('./Arrow') const Async = require('./Async') const Const = require('./Const') const Either = require('./Either') +const Equiv = require('./Equiv') const Identity = require('./Identity') const IO = require('./IO') const List = require('./List') @@ -101,6 +102,7 @@ const bimap = require('./pointfree/bimap') const both = require('./pointfree/both') const chain = require('./pointfree/chain') const coalesce = require('./pointfree/coalesce') +const compareWith = require('./pointfree/compareWith') const concat = require('./pointfree/concat') const cons = require('./pointfree/cons') const contramap = require('./pointfree/contramap') @@ -215,6 +217,7 @@ test('entry', t => { t.equal(crocks.Async, Async, 'provides the Async crock') t.equal(crocks.Const, Const, 'provides the Const crock') t.equal(crocks.Either, Either, 'provides the Either crock') + t.equal(crocks.Equiv, Equiv, 'provides the Equiv crock') t.equal(crocks.Identity, Identity, 'provides the Identity crock') t.equal(crocks.IO, IO, 'provides the IO crock') t.equal(crocks.List, List, 'provides the List crock') @@ -300,6 +303,7 @@ test('entry', t => { t.equal(crocks.bimap, bimap, 'provides the bimap pointfree') t.equal(crocks.both, both, 'provides the both pointfree') t.equal(crocks.chain, chain, 'provides the chain pointfree') + t.equal(crocks.compareWith, compareWith, 'provides the compareWith pointfree') t.equal(crocks.coalesce, coalesce, 'provides the coalesce pointfree') t.equal(crocks.concat, concat, 'provides the concat pointfree') t.equal(crocks.cons, cons, 'provides the cons pointfree') diff --git a/src/pointfree/compareWith.js b/src/pointfree/compareWith.js new file mode 100644 index 000000000..63a8c2743 --- /dev/null +++ b/src/pointfree/compareWith.js @@ -0,0 +1,15 @@ +/** @license ISC License (c) copyright 2017 original and current authors */ +/** @author Ian Hofmann-Hicks (evil) */ + +const curry = require('../core/curry') +const isFunction = require('../core/isFunction') + +function compareWith(x, y, m) { + if(!(m && isFunction(m.compareWith))) { + throw new TypeError('compareWith: Equiv required for third argument') + } + + return m.compareWith(x, y) +} + +module.exports = curry(compareWith) diff --git a/src/pointfree/compareWith.spec.js b/src/pointfree/compareWith.spec.js new file mode 100644 index 000000000..a44ca89d6 --- /dev/null +++ b/src/pointfree/compareWith.spec.js @@ -0,0 +1,40 @@ +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 compareWith = require('./compareWith') + +test('compareWith pointfree', t => { + const f = bindFunc(compareWith) + const x = 'result' + const m = { compareWith: sinon.spy(constant(x)) } + + t.ok(isFunction(compareWith), 'is a function') + + const err = /compareWith: Equiv required for third argument/ + t.throws(f(13, 13, undefined), err, 'throws if passed undefined') + t.throws(f(13, 13, null), err, 'throws if passed null') + t.throws(f(13, 13, 0), err, 'throws if passed a falsey number') + t.throws(f(13, 13, 1), err, 'throws if passed a truthy number') + t.throws(f(13, 13, ''), err, 'throws if passed a falsey string') + t.throws(f(13, 13, 'string'), err, 'throws if passed a truthy string') + t.throws(f(13, 13, false), err, 'throws if passed false') + t.throws(f(13, 13, true), err, 'throws if passed true') + t.throws(f(13, 13, []), err, 'throws if passed an array') + t.throws(f(13, 13, {}), err, 'throws if passed an object') + t.throws(f(13, 13, unit), err, 'throws if passed a function') + + const result = compareWith(23)(23)(m) + + t.ok(m.compareWith.called, 'calls compareWith on the passed container') + t.equal(result, x, 'returns the result of calling m.compareWith') + + t.end() +}) diff --git a/src/pointfree/runWith.js b/src/pointfree/runWith.js index b99eaafda..d4ce55db2 100644 --- a/src/pointfree/runWith.js +++ b/src/pointfree/runWith.js @@ -6,7 +6,7 @@ const isFunction = require('../core/isFunction') function runWith(x, m) { if(!(m && isFunction(m.runWith))) { - throw new TypeError('runWith: Arrow, Reader, Star or State required for second argument') + throw new TypeError('runWith: Arrow, Endo, Pred, Reader, Star or State required for second argument') } return m.runWith(x) diff --git a/src/pointfree/runWith.spec.js b/src/pointfree/runWith.spec.js index 23920d80c..3fbcdf8ce 100644 --- a/src/pointfree/runWith.spec.js +++ b/src/pointfree/runWith.spec.js @@ -18,17 +18,18 @@ test('runWith pointfree', t => { t.ok(isFunction(runWith), 'is a function') - t.throws(f(13, undefined), TypeError, 'throws if passed undefined') - t.throws(f(13, null), TypeError, 'throws if passed null') - t.throws(f(13, 0), TypeError, 'throws if passed a falsey number') - t.throws(f(13, 1), TypeError, 'throws if passed a truthy number') - t.throws(f(13, ''), TypeError, 'throws if passed a falsey string') - t.throws(f(13, 'string'), TypeError, 'throws if passed a truthy string') - t.throws(f(13, false), TypeError, 'throws if passed false') - t.throws(f(13, true), TypeError, 'throws if passed true') - t.throws(f(13, []), TypeError, 'throws if passed an array') - t.throws(f(13, {}), TypeError, 'throws if passed an object') - t.throws(f(13, unit), TypeError, 'throws if passed a function') + const err = /runWith: Arrow, Endo, Pred, Reader, Star or State required for second argument/ + t.throws(f(13, undefined), err, 'throws if passed undefined') + t.throws(f(13, null), err, 'throws if passed null') + t.throws(f(13, 0), err, 'throws if passed a falsey number') + t.throws(f(13, 1), err, 'throws if passed a truthy number') + t.throws(f(13, ''), err, 'throws if passed a falsey string') + t.throws(f(13, 'string'), err, 'throws if passed a truthy string') + t.throws(f(13, false), err, 'throws if passed false') + t.throws(f(13, true), err, 'throws if passed true') + t.throws(f(13, []), err, 'throws if passed an array') + t.throws(f(13, {}), err, 'throws if passed an object') + t.throws(f(13, unit), err, 'throws if passed a function') const result = runWith(23)(m)