diff --git a/README.md b/README.md index e2841c683..274d15cf9 100644 --- a/README.md +++ b/README.md @@ -76,15 +76,15 @@ All `Crocks` are Constructor functions of the given type, with `Writer` being an | `Arrow` | `empty` | `both`, `concat`, `contramap`, `empty`, `first`, `map`, `promap`, `runWith`, `second`, `value` | | `Async` | `Rejected`, `Resolved`, `all`, `fromNode`, `fromPromise`, `of` | `alt`, `ap`, `bimap`, `chain`, `coalesce`, `fork`, `map`, `of`, `swap`, `toPromise` | | `Const` | -- | `ap`, `chain`, `concat`, `equals`, `map`, `value` | -| `Either` | `Left`, `Right`, `of`| `alt`, `ap`, `bimap`, `chain`, `coalesce`, `either`, `equals`, `map`, `of`, `sequence`, `swap`, `traverse` | -| `Identity` | `of` | `ap`, `chain`, `equals`, `map`, `of`, `sequence`, `traverse`, `value` | +| `Either` | `Left`, `Right`, `of`| `alt`, `ap`, `bimap`, `chain`, `coalesce`, `concat`, `either`, `equals`, `map`, `of`, `sequence`, `swap`, `traverse` | +| `Identity` | `of` | `ap`, `chain`, `concat`, `equals`, `map`, `of`, `sequence`, `traverse`, `value` | | `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`, `value` | -| `Maybe` | `Nothing`, `Just`, `of`, `zero` | `alt`, `ap`, `chain`, `coalesce`, `equals`, `either`, `map`, `of`, `option`, `sequence`, `traverse`, `zero` | +| `Maybe` | `Nothing`, `Just`, `of`, `zero` | `alt`, `ap`, `chain`, `coalesce`, `concat`, `equals`, `either`, `map`, `of`, `option`, `sequence`, `traverse`, `zero` | | `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` | -- | `both`, `contramap`, `map`, `promap`, `runWith` | +| `Star` | -- | `both`, `concat`, `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` | @@ -308,7 +308,7 @@ These functions provide a very clean way to build out very simple functions and | `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` | +| `concat` | `All`, `Any`, `Array`, `Arrow`, `Assign`, `Const`, `Either`, `Identity`, `List`, `Max`, `Maybe`, `Min`, `Pair`, `Pred`, `Prod`, `Star`, `String`, `Sum`, `Unit` | | `cons` | `Array`, `List` | | `contramap` | `Arrow`, `Pred`, `Star` | | `either` | `Either`, `Maybe` | diff --git a/crocks/Either.js b/crocks/Either.js index 383b08f2a..dec9a9fcf 100644 --- a/crocks/Either.js +++ b/crocks/Either.js @@ -7,6 +7,7 @@ const isSameType = require('../predicates/isSameType') const _inspect = require('../internal/inspect') const defineUnion = require('../internal/defineUnion') +const innerConcat = require('../internal/innerConcat') const constant = require('../combinators/constant') const composeB = require('../combinators/composeB') @@ -75,6 +76,17 @@ function Either(u) { }, x) } + function concat(m) { + if(!isSameType(Either, m)) { + throw new TypeError('Either.concat: Either of Semigroup required') + } + + return either( + Either.Left, + innerConcat(Either, m) + ) + } + function swap(f, g) { if(!isFunction(f) || !isFunction(g)) { throw new TypeError('Either.swap: Requires both left and right functions') @@ -178,7 +190,7 @@ function Either(u) { } return { - inspect, either, type, + inspect, either, type, concat, swap, coalesce, equals, map, bimap, alt, ap, of, chain, sequence, traverse } diff --git a/crocks/Either.spec.js b/crocks/Either.spec.js index 92ab9dedb..ce7a19e2b 100644 --- a/crocks/Either.spec.js +++ b/crocks/Either.spec.js @@ -5,8 +5,11 @@ const helpers = require('../test/helpers') const noop = helpers.noop const bindFunc = helpers.bindFunc -const isObject = require('../predicates/isObject') + +const isArray = require('../predicates/isArray') const isFunction = require('../predicates/isFunction') +const isObject = require('../predicates/isObject') +const isSameType = require('../predicates/isSameType') const composeB = require('../combinators/composeB') const constant = require('../combinators/constant') @@ -172,6 +175,114 @@ test('Either coalesce', t => { t.end() }) +test('Either concat errors', t => { + const m = { type: () => 'Either...Not' } + + const good = Either.Right([]) + const bad = Either.Left([]) + + const f = bindFunc(Either.Right([]).concat) + const nonEitherErr = /Either.concat: Either of Semigroup required/ + + t.throws(f(undefined), nonEitherErr, 'throws with undefined on Right') + t.throws(f(null), nonEitherErr, 'throws with null on Right') + t.throws(f(0), nonEitherErr, 'throws with falsey number on Right') + t.throws(f(1), nonEitherErr, 'throws with truthy number on Right') + t.throws(f(''), nonEitherErr, 'throws with falsey string on Right') + t.throws(f('string'), nonEitherErr, 'throws with truthy string on Right') + t.throws(f(false), nonEitherErr, 'throws with false on Right') + t.throws(f(true), nonEitherErr, 'throws with true on Right') + t.throws(f([]), nonEitherErr, 'throws with array on Right') + t.throws(f({}), nonEitherErr, 'throws with object on Right') + t.throws(f(m), nonEitherErr, 'throws with non-Either on Right') + + const g = bindFunc(Either.Left(0).concat) + + t.throws(g(undefined), nonEitherErr, 'throws with undefined on Left') + t.throws(g(null), nonEitherErr, 'throws with null on Left') + t.throws(g(0), nonEitherErr, 'throws with falsey number on Left') + t.throws(g(1), nonEitherErr, 'throws with truthy number on Left') + t.throws(g(''), nonEitherErr, 'throws with falsey string on Left') + t.throws(g('string'), nonEitherErr, 'throws with truthy string on Left') + t.throws(g(false), nonEitherErr, 'throws with false on Left') + t.throws(g(true), nonEitherErr, 'throws with true on Left') + t.throws(g([]), nonEitherErr, 'throws with array on Left') + t.throws(g({}), nonEitherErr, 'throws with object on Left') + t.throws(g(m), nonEitherErr, 'throws with non-Either on Left') + + const innerErr = /Either.concat: Both containers must contain Semigroups of the same type/ + const notSemiLeft = bindFunc(x => Either.Right(x).concat(good)) + + t.throws(notSemiLeft(undefined), innerErr, 'throws with undefined on left') + t.throws(notSemiLeft(null), innerErr, 'throws with null on left') + t.throws(notSemiLeft(0), innerErr, 'throws with falsey number on left') + t.throws(notSemiLeft(1), innerErr, 'throws with truthy number on left') + t.throws(notSemiLeft(''), innerErr, 'throws with falsey string on left') + t.throws(notSemiLeft('string'), innerErr, 'throws with truthy string on left') + t.throws(notSemiLeft(false), innerErr, 'throws with false on left') + t.throws(notSemiLeft(true), innerErr, 'throws with true on left') + t.throws(notSemiLeft({}), innerErr, 'throws with object on left') + + const notSemiRight = bindFunc(x => good.concat(Either.Right(x))) + + t.throws(notSemiRight(undefined), innerErr, 'throws with undefined on right') + t.throws(notSemiRight(null), innerErr, 'throws with null on right') + t.throws(notSemiRight(0), innerErr, 'throws with falsey number on right') + t.throws(notSemiRight(1), innerErr, 'throws with truthy number on right') + t.throws(notSemiRight(''), innerErr, 'throws with falsey string on right') + t.throws(notSemiRight('string'), innerErr, 'throws with truthy string on right') + t.throws(notSemiRight(false), innerErr, 'throws with false on right') + t.throws(notSemiRight(true), innerErr, 'throws with true on right') + t.throws(notSemiRight({}), innerErr, 'throws with object on right') + + const noMatch = bindFunc(() => good.concat(Either.Right(''))) + t.throws(noMatch({}), innerErr, 'throws with different semigroups') + + t.end() +}) + +test('Either concat functionality', t => { + const extract = + either(identity, identity) + + const left = Either.Left('Left') + const a = Either.Right([ 1, 2 ]) + const b = Either.Right([ 4, 3 ]) + + const right = a.concat(b) + const leftRight = a.concat(left) + const leftLeft = left.concat(a) + + t.ok(isSameType(Either, right), 'returns another Either with Right') + t.ok(isSameType(Either, leftRight), 'returns another Either with Left on right side') + t.ok(isSameType(Either, leftLeft), 'returns another Either with Left on left side') + + t.same(extract(right), [ 1, 2, 4, 3 ], 'concats the inner semigroup with Rights') + t.equals(extract(leftRight), 'Left', 'returns a Left with a Left on right side') + t.equals(extract(leftLeft), 'Left', 'returns a Left with a Left on left side') + + t.end() +}) + +test('Either concat properties (Semigoup)', t => { + const extract = + either(identity, identity) + + const a = Either.Right([ 'a' ]) + const b = Either.Right([ 'b' ]) + const c = Either.Right([ 'c' ]) + + const left = a.concat(b).concat(c) + const right = a.concat(b.concat(c)) + + t.ok(isFunction(a.concat), 'provides a concat function') + + t.same(extract(left), extract(right), 'associativity') + t.ok(isArray(extract(a.concat(b))), 'returns an Array') + + t.end() +}) + test('Either equals functionality', t => { const la = Either.Left(0) const lb = Either.Left(0) diff --git a/crocks/Identity.js b/crocks/Identity.js index f44c6cc2f..b5fe68ff1 100644 --- a/crocks/Identity.js +++ b/crocks/Identity.js @@ -6,6 +6,7 @@ const isFunction = require('../predicates/isFunction') const isSameType = require('../predicates/isSameType') const _inspect = require('../internal/inspect') +const innerConcat = require('../internal/innerConcat') const composeB = require('../combinators/composeB') const constant = require('../combinators/constant') @@ -37,6 +38,14 @@ function Identity(x) { const inspect = constant(`Identity${_inspect(x)}`) + function concat(m) { + if(!isSameType(Identity, m)) { + throw new TypeError('Identity.concat: Identity of Semigroup required') + } + + return innerConcat(Identity, m, x) + } + function map(fn) { if(!isFunction(fn)) { throw new TypeError('Identity.map: Function required') @@ -97,8 +106,8 @@ function Identity(x) { return { inspect, value, type, equals, - map, ap, of, chain, sequence, - traverse + concat, map, ap, of, chain, + sequence, traverse } } diff --git a/crocks/Identity.spec.js b/crocks/Identity.spec.js index ce03b203a..f07c6c029 100644 --- a/crocks/Identity.spec.js +++ b/crocks/Identity.spec.js @@ -2,8 +2,10 @@ const test = require('tape') const sinon = require('sinon') const helpers = require('../test/helpers') -const isObject = require('../predicates/isObject') +const isArray = require('../predicates/isArray') const isFunction = require('../predicates/isFunction') +const isObject = require('../predicates/isObject') +const isSameType = require('../predicates/isSameType') const bindFunc = helpers.bindFunc const noop = helpers.noop @@ -88,6 +90,76 @@ test('Identity equals properties (Setoid)', t => { t.end() }) +test('Identity concat errors', t => { + const m = { type: () => 'Identity...Not' } + + const good = Identity([]) + + const f = bindFunc(Identity([]).concat) + const nonIdentErr = /Identity.concat: Identity of Semigroup required/ + + t.throws(f(undefined), nonIdentErr, 'throws with undefined') + t.throws(f(null), nonIdentErr, 'throws with null') + t.throws(f(0), nonIdentErr, 'throws with falsey number') + t.throws(f(1), nonIdentErr, 'throws with truthy number') + t.throws(f(''), nonIdentErr, 'throws with falsey string') + t.throws(f('string'), nonIdentErr, 'throws with truthy string') + t.throws(f(false), nonIdentErr, 'throws with false') + t.throws(f(true), nonIdentErr, 'throws with true') + t.throws(f([]), nonIdentErr, 'throws with array') + t.throws(f({}), nonIdentErr, 'throws with object') + t.throws(f(m), nonIdentErr, 'throws with non-Identity') + + const innerErr = /Identity.concat: Both containers must contain Semigroups of the same type/ + const notSemi = bindFunc(x => Identity(x).concat(good)) + + t.throws(notSemi(undefined), innerErr, 'throws with undefined on left') + t.throws(notSemi(null), innerErr, 'throws with null on left') + t.throws(notSemi(0), innerErr, 'throws with falsey number on left') + t.throws(notSemi(1), innerErr, 'throws with truthy number on left') + t.throws(notSemi(''), innerErr, 'throws with falsey string on left') + t.throws(notSemi('string'), innerErr, 'throws with truthy string on left') + t.throws(notSemi(false), innerErr, 'throws with false on left') + t.throws(notSemi(true), innerErr, 'throws with true on left') + t.throws(notSemi({}), innerErr, 'throws with object on left') + + const noMatch = bindFunc(() => good.concat(Identity(''))) + t.throws(noMatch({}), innerErr, 'throws with different semigroups') + + t.end() +}) + +test('Identity concat functionality', t => { + const a = Identity([ 1, 2 ]) + const b = Identity([ 4, 3 ]) + + const res = a.concat(b) + + t.ok(isSameType(Identity, res), 'returns another Identity') + t.same(res.value(), [ 1, 2, 4, 3 ], 'concats the inner semigroup with Rights') + + t.end() +}) + +test('Identity concat properties (Semigoup)', t => { + const extract = + m => m.value() + + const a = Identity([ 'a' ]) + const b = Identity([ 'b' ]) + const c = Identity([ 'c' ]) + + const left = a.concat(b).concat(c) + const right = a.concat(b.concat(c)) + + t.ok(isFunction(a.concat), 'provides a concat function') + + t.same(extract(left), extract(right), 'associativity') + t.ok(isArray(extract(a.concat(b))), 'returns an Array') + + t.end() +}) + test('Identity map errors', t => { const map = bindFunc(Identity(0).map) diff --git a/crocks/Maybe.js b/crocks/Maybe.js index 1f23d53d1..9b04150cb 100644 --- a/crocks/Maybe.js +++ b/crocks/Maybe.js @@ -7,6 +7,7 @@ const isSameType = require('../predicates/isSameType') const _inspect = require('../internal/inspect') const defineUnion = require('../internal/defineUnion') +const innerConcat = require('../internal/innerConcat') const composeB = require('../combinators/composeB') const constant = require('../combinators/constant') @@ -76,6 +77,17 @@ function Maybe(u) { }, x) } + function concat(m) { + if(!isSameType(Maybe, m)) { + throw new TypeError('Maybe.concat: Maybe of Semigroup required') + } + + return either( + Maybe.Nothing, + innerConcat(Maybe, m) + ) + } + function coalesce(f, g) { if(!isFunction(f) || !isFunction(g)) { throw new TypeError('Maybe.coalesce: Requires both left and right functions') @@ -174,7 +186,7 @@ function Maybe(u) { return { inspect, either, option, type, - equals, coalesce, map, alt, + concat, equals, coalesce, map, alt, zero, ap, of, chain, sequence, traverse } diff --git a/crocks/Maybe.spec.js b/crocks/Maybe.spec.js index c963435c8..658be0e6e 100644 --- a/crocks/Maybe.spec.js +++ b/crocks/Maybe.spec.js @@ -5,8 +5,10 @@ const helpers = require('../test/helpers') const noop = helpers.noop const bindFunc = helpers.bindFunc +const isArray = require('../predicates/isArray') const isFunction = require('../predicates/isFunction') const isObject = require('../predicates/isObject') +const isSameType = require('../predicates/isSameType') const composeB = require('../combinators/composeB') const constant = require('../combinators/constant') @@ -132,6 +134,99 @@ test('Maybe coalesce', t => { t.end() }) +test('Maybe concat errors', t => { + const m = { type: () => 'Maybe...Not' } + + const good = Maybe.of([]) + + const f = bindFunc(Maybe.of([]).concat) + const nonMaybeErr = /Maybe.concat: Maybe of Semigroup required/ + + t.throws(f(undefined), nonMaybeErr, 'throws with undefined') + t.throws(f(null), nonMaybeErr, 'throws with null') + t.throws(f(0), nonMaybeErr, 'throws with falsey number') + t.throws(f(1), nonMaybeErr, 'throws with truthy number') + t.throws(f(''), nonMaybeErr, 'throws with falsey string') + t.throws(f('string'), nonMaybeErr, 'throws with truthy string') + t.throws(f(false), nonMaybeErr, 'throws with false') + t.throws(f(true), nonMaybeErr, 'throws with true') + t.throws(f([]), nonMaybeErr, 'throws with array') + t.throws(f({}), nonMaybeErr, 'throws with object') + t.throws(f(m), nonMaybeErr, 'throws with non-Maybe') + + const innerErr = /Maybe.concat: Both containers must contain Semigroups of the same type/ + const notSemiLeft = bindFunc(x => Maybe.of(x).concat(good)) + + t.throws(notSemiLeft(undefined), innerErr, 'throws with undefined on left') + t.throws(notSemiLeft(null), innerErr, 'throws with null on left') + t.throws(notSemiLeft(0), innerErr, 'throws with falsey number on left') + t.throws(notSemiLeft(1), innerErr, 'throws with truthy number on left') + t.throws(notSemiLeft(''), innerErr, 'throws with falsey string on left') + t.throws(notSemiLeft('string'), innerErr, 'throws with truthy string on left') + t.throws(notSemiLeft(false), innerErr, 'throws with false on left') + t.throws(notSemiLeft(true), innerErr, 'throws with true on left') + t.throws(notSemiLeft({}), innerErr, 'throws with object on left') + + const notSemiRight = bindFunc(x => good.concat(Maybe.of(x))) + + t.throws(notSemiRight(undefined), innerErr, 'throws with undefined on right') + t.throws(notSemiRight(null), innerErr, 'throws with null on right') + t.throws(notSemiRight(0), innerErr, 'throws with falsey number on right') + t.throws(notSemiRight(1), innerErr, 'throws with truthy number on right') + t.throws(notSemiRight(''), innerErr, 'throws with falsey string on right') + t.throws(notSemiRight('string'), innerErr, 'throws with truthy string on right') + t.throws(notSemiRight(false), innerErr, 'throws with false on right') + t.throws(notSemiRight(true), innerErr, 'throws with true on right') + t.throws(notSemiRight({}), innerErr, 'throws with object on right') + + const noMatch = bindFunc(() => good.concat(Maybe.of(''))) + t.throws(noMatch({}), innerErr, 'throws with different semigroups') + + t.end() +}) + +test('Maybe concat functionality', t => { + const extract = + either(constant('Nothing'), identity) + + const nothing = Maybe.Nothing() + const a = Maybe.Just([ 1, 2 ]) + const b = Maybe.Just([ 4, 3 ]) + + const just = a.concat(b) + const nothingRight = a.concat(nothing) + const nothingLeft = nothing.concat(a) + + t.ok(isSameType(Maybe, just), 'returns anothor Maybe with Just') + t.ok(isSameType(Maybe, nothingRight), 'returns anothor Maybe with Nothing on Right') + t.ok(isSameType(Maybe, nothingLeft), 'returns anothor Maybe with Nothing on Left') + + t.same(extract(just), [ 1, 2, 4, 3 ], 'concats the inner semigroup with Justs') + t.equals(extract(nothingRight), 'Nothing', 'returns a Nothing with a Nothing on Right') + t.equals(extract(nothingLeft), 'Nothing', 'returns a Nothing with a Nothing on Left') + + t.end() +}) + +test('Maybe concat properties (Semigoup)', t => { + const extract = + either(constant('Nothing'), identity) + + const a = Maybe.Just([ 'a' ]) + const b = Maybe.Just([ 'b' ]) + const c = Maybe.Just([ 'c' ]) + + const left = a.concat(b).concat(c) + const right = a.concat(b.concat(c)) + + t.ok(isFunction(a.concat), 'provides a concat function') + + t.same(extract(left), extract(right), 'associativity') + t.ok(isArray(extract(a.concat(b))), 'returns an Array') + + t.end() +}) + test('Maybe equals functionality', t => { const a = Maybe.Just(0) const b = Maybe.Just(0) @@ -171,6 +266,7 @@ test('Maybe equals properties (Setoid)', t => { }) test('Maybe map errors', t => { + const m = { type: () => 'Maybe...Not' } const map = bindFunc(Maybe.Just(0).map) t.throws(map(undefined), TypeError, 'throws with undefined') @@ -182,7 +278,8 @@ test('Maybe map errors', t => { t.throws(map(false), TypeError, 'throws with false') t.throws(map(true), TypeError, 'throws with true') t.throws(map([]), TypeError, 'throws with an array') - t.throws(map({}), TypeError, 'throws iwth object') + t.throws(map({}), TypeError, 'throws with object') + t.throws(map(m), TypeError, 'throws with non-Maybe') t.doesNotThrow(map(noop), 'allows a function') diff --git a/internal/innerConcat.js b/internal/innerConcat.js new file mode 100644 index 000000000..3e658222a --- /dev/null +++ b/internal/innerConcat.js @@ -0,0 +1,26 @@ +/** @license ISC License (c) copyright 2017 original and current authors */ +/** @author Ian Hofmann-Hicks (evil) */ + +const curry = require('../helpers/curry') +const isSameType = require('../predicates/isSameType') +const isSemigroup = require('../predicates/isSemigroup') + +function innerConcat(type, m) { + const t = type.type() + + return function(left) { + if(!isSemigroup(left)) { + throw new TypeError(`${t}.concat: Both containers must contain Semigroups of the same type`) + } + + return m.map(right => { + if(!isSameType(left, right)) { + throw new TypeError(`${t}.concat: Both containers must contain Semigroups of the same type`) + } + + return left.concat(right) + }) + } +} + +module.exports = curry(innerConcat) diff --git a/internal/innerConcat.spec.js b/internal/innerConcat.spec.js new file mode 100644 index 000000000..372ca0574 --- /dev/null +++ b/internal/innerConcat.spec.js @@ -0,0 +1,47 @@ +const test = require('tape') +const helpers = require('../test/helpers') + +const bindFunc = helpers.bindFunc + +const isFunction = require('../predicates/isFunction') +const isSameType = require('../predicates/isSameType') + +const innerConcat = require('./innerConcat') + +const Mock = require('../test/MockCrock') +const Last = require('../test/LastMonoid') + +test('innerConcat errors', t => { + const err = /MockCrock.concat\: / + + const outer = bindFunc(x => innerConcat(Mock, Mock.of(Last(4)))(x)) + + t.throws(outer(undefined), err, 'throws interpolated error when left container contains undefined') + t.throws(outer(null), err, 'throws interpolated error when left container contains null') + t.throws(outer(0), err, 'throws interpolated error when left container contains falsey number') + t.throws(outer(1), err, 'throws interpolated error when left container contains truthy number') + t.throws(outer(''), err, 'throws interpolated error when left container contains falsey string') + t.throws(outer('string'), err, 'throws interpolated error when left container contains truthy string') + t.throws(outer(false), err, 'throws interpolated error when left container contains false') + t.throws(outer(true), err, 'throws interpolated error when left container contains true') + t.throws(outer({}), err, 'throws interpolated error when left container contains an object') + + t.end() +}) + +test('innerConcat functionality', t => { + t.ok(isFunction(innerConcat), 'is a function') + + const val = 4 + const right = Last(val) + const left = Last(99) + + const fn = innerConcat(Mock, Mock.of(right)) + const res = fn(left) + + t.ok(isSameType(Mock, res), 'returns a container of the same type') + t.ok(isSameType(Last, res.value()), 'new value is a Monoid of the expected type') + t.equals(res.value().value(), val, 'new value is a Monoid of the expected type') + + t.end() +}) diff --git a/predicates/isSameType.js b/predicates/isSameType.js index f043a398a..c4f5f66dc 100644 --- a/predicates/isSameType.js +++ b/predicates/isSameType.js @@ -2,14 +2,36 @@ /** @author Ian Hofmann-Hicks (evil) */ const curry = require('../helpers/curry') +const isArray = require('../predicates/isArray') const isFunction = require('../predicates/isFunction') -const isString = require('../predicates/isString') +const isNil = require('../predicates/isNil') -// isSameType :: Container m => (String | m) -> m -> Boolean -function isSameType(type, m) { - return !!type && !!m - && isFunction(type.type) && isFunction(m.type) - && type.type() === m.type() +function isAdt(x) { + return !!x && isFunction(x.type) +} + +function adtType(x, y) { + return isAdt(x) + && isAdt(y) + && x.type() === y.type() +} + +function typeName(x) { + return isArray(x) ? 'array' : typeof x +} + +function typeRep(x, y) { + return x.name === y.constructor.name + || y.name === x.constructor.name +} + +// isSameType :: Container m => m -> m -> Boolean +function isSameType(x, y) { + if(isAdt(x) || isAdt(y)) { return adtType(x, y) } + else { + if(isNil(x) || isNil(y)) { return x === y } + return typeRep(x, y) || typeName(x) === typeName(y) + } } module.exports = curry(isSameType) diff --git a/predicates/isSameType.spec.js b/predicates/isSameType.spec.js index 73b01bcaf..04e769c1c 100644 --- a/predicates/isSameType.spec.js +++ b/predicates/isSameType.spec.js @@ -1,6 +1,8 @@ const test = require('tape') +const helpers = require('../test/helpers') const isFunction = require('../predicates/isFunction') +const noop = helpers.noop const isSameType = require('./isSameType') @@ -10,9 +12,317 @@ test('isSameType predicate function', t => { t.ok(isFunction(isSameType), 'is a function') + t.end() +}) + +test('isSameType (ADTs)', t => { + const first = { type: () => 'first' } + const second = { type: () => 'second' } + t.equal(isSameType(first, first), true, 'reports true when they are the same') t.equal(isSameType(first, second), false, 'reports false when they are the different containers') t.equal(isSameType(first, []), false, 'reports false when one is not a container') t.end() }) + +test('isSameType (Nils)', t => { + t.equal(isSameType(undefined, undefined), true, 'reports true when both are undefined') + t.equal(isSameType(null, null), true, 'reports true when both are null') + + t.equal(isSameType(undefined, null), false, 'reports false with undefined and null') + t.equal(isSameType(undefined, 0), false, 'reports false with undefined and falsey number') + t.equal(isSameType(undefined, 1), false, 'reports false with undefined and truthy number') + t.equal(isSameType(undefined, ''), false, 'reports false with undefined and falsey string') + t.equal(isSameType(undefined, 'string'), false, 'reports false with undefined and truthy string') + t.equal(isSameType(undefined, false), false, 'reports false with undefined and false') + t.equal(isSameType(undefined, true), false, 'reports false with undefined and true') + t.equal(isSameType(undefined, []), false, 'reports false with undefined and array') + t.equal(isSameType(undefined, {}), false, 'reports false with undefined and an object') + t.equal(isSameType(undefined, noop), false, 'reports false with undefined and a function') + + t.equal(isSameType(null, undefined), false, 'reports false with null and undefined') + t.equal(isSameType(0, undefined), false, 'reports false with falsey number and undefined') + t.equal(isSameType(1, undefined), false, 'reports false with truthy number and undefined') + t.equal(isSameType('', undefined), false, 'reports false with falsey string and undefined') + t.equal(isSameType('string', undefined), false, 'reports false with truthy string and undefined') + t.equal(isSameType(false, undefined), false, 'reports false with false and undefined') + t.equal(isSameType(true, undefined), false, 'reports false with true and undefined') + t.equal(isSameType([], undefined), false, 'reports false with array and undefined') + t.equal(isSameType({}, undefined), false, 'reports false with an object and undefined') + t.equal(isSameType(noop, undefined), false, 'reports false with a function and undefined') + + t.equal(isSameType(null, undefined), false, 'reports false null and undefined') + t.equal(isSameType(null, 0), false, 'reports false with null and falsey number') + t.equal(isSameType(null, 1), false, 'reports false with null and truthy number') + t.equal(isSameType(null, ''), false, 'reports false with null and falsey string') + t.equal(isSameType(null, 'string'), false, 'reports false with null and truthy string') + t.equal(isSameType(null, false), false, 'reports false with null and false') + t.equal(isSameType(null, true), false, 'reports false with null and true') + t.equal(isSameType(null, []), false, 'reports false with null and array') + t.equal(isSameType(null, {}), false, 'reports false with null and an object') + t.equal(isSameType(null, noop), false, 'reports false with null and a function') + + t.equal(isSameType(null, undefined), false, 'reports false undefined and null') + t.equal(isSameType(0, null), false, 'reports false with falsey number and null') + t.equal(isSameType(1, null), false, 'reports false with truthy number and null') + t.equal(isSameType('', null), false, 'reports false with falsey string and null') + t.equal(isSameType('string', null), false, 'reports false with truthy string and null') + t.equal(isSameType(false, null), false, 'reports false with false and null') + t.equal(isSameType(true, null), false, 'reports false with true and null') + t.equal(isSameType([], null), false, 'reports false with array and null') + t.equal(isSameType({}, null), false, 'reports false with an object and null') + t.equal(isSameType(noop, null), false, 'reports false with a function and null') + + t.end() +}) + +test('isSameType (Numbers)', t => { + t.equal(isSameType(Number, 0), true, 'reports true with Number and falsey number') + t.equal(isSameType(Number, 1), true, 'reports true with Number and truthy number') + t.equal(isSameType(0, Number), true, 'reports true with falsey number and Number') + t.equal(isSameType(1, Number), true, 'reports true with truthy number and Number') + + t.equal(isSameType(0, 1), true, 'reports true with falsey number and truthy number') + t.equal(isSameType(1, 0), true, 'reports true with truthy number and falsey number') + + t.equal(isSameType(Number, ''), false, 'reports false with Number and falsey string') + t.equal(isSameType(Number, 'string'), false, 'reports false with Number and truthy string') + t.equal(isSameType(Number, false), false, 'reports false with Number and false') + t.equal(isSameType(Number, true), false, 'reports false with Number and true') + t.equal(isSameType(Number, {}), false, 'reports false with Number and object') + t.equal(isSameType(Number, []), false, 'reports false with Number and array') + + t.equal(isSameType('', Number), false, 'reports false with falsey string and Number') + t.equal(isSameType('string', Number), false, 'reports false with truthy string and Number') + t.equal(isSameType(false, Number), false, 'reports false with false and Number') + t.equal(isSameType(true, Number), false, 'reports false with true and Number') + t.equal(isSameType({}, Number), false, 'reports false with object and Number') + t.equal(isSameType([], Number), false, 'reports false with array and Number') + + t.equal(isSameType(0, ''), false, 'reports false with falsey number and falsey string') + t.equal(isSameType(0, 'string'), false, 'reports false with falsey number and truthy string') + t.equal(isSameType(0, false), false, 'reports false with falsey number and false') + t.equal(isSameType(0, true), false, 'reports false with falsey number and true') + t.equal(isSameType(0, {}), false, 'reports false with falsey number and object') + t.equal(isSameType(0, []), false, 'reports false with falsey number and array') + t.equal(isSameType(0, noop), false, 'reports false with falsey number and function') + + t.equal(isSameType('', 0), false, 'reports false with falsey string and falsey number') + t.equal(isSameType('string', 0), false, 'reports false with truthy string and falsey number') + t.equal(isSameType(false, 0), false, 'reports false with false and falsey number') + t.equal(isSameType(true, 0), false, 'reports false with true and falsey number') + t.equal(isSameType({}, 0), false, 'reports false with object and falsey number') + t.equal(isSameType([], 0), false, 'reports false with array and falsey number') + t.equal(isSameType(noop, 0), false, 'reports false with function and falsey number') + + t.equal(isSameType(1, ''), false, 'reports false with truthy number and falsey string') + t.equal(isSameType(1, 'string'), false, 'reports false with truthy number and truthy string') + t.equal(isSameType(1, false), false, 'reports false with truthy number and false') + t.equal(isSameType(1, true), false, 'reports false with truthy number and true') + t.equal(isSameType(1, {}), false, 'reports false with truthy number and object') + t.equal(isSameType(1, []), false, 'reports false with truthy number and array') + t.equal(isSameType(1, noop), false, 'reports false with truthy number and function') + + t.equal(isSameType('', 1), false, 'reports false with falsey string and truthy number') + t.equal(isSameType('string', 1), false, 'reports false with truthy string and truthy number') + t.equal(isSameType(false, 1), false, 'reports false with false and truthy number') + t.equal(isSameType(true, 1), false, 'reports false with true and truthy number') + t.equal(isSameType({}, 1), false, 'reports false with object and truthy number') + t.equal(isSameType([], 1), false, 'reports false with array and truthy number') + t.equal(isSameType(noop, 1), false, 'reports false with function and truthy number') + + t.end() +}) + +test('isSameType (Strings)', t => { + t.equal(isSameType(String, ''), true, 'reports true with String and falsey string') + t.equal(isSameType(String, 'string'), true, 'reports true with String and truthy string') + t.equal(isSameType('', String), true, 'reports true with falsey string and String') + t.equal(isSameType('string', String), true, 'reports true with truthy string and String') + + t.equal(isSameType('', 'string'), true, 'reports true with falsey string and truthy string') + t.equal(isSameType('string', ''), true, 'reports true with truthy string and falsey string') + + t.equal(isSameType(String, 0), false, 'reports false with String and falsey number') + t.equal(isSameType(String, 1), false, 'reports false with String and truthy number') + t.equal(isSameType(String, false), false, 'reports false with String and false') + t.equal(isSameType(String, true), false, 'reports false with String and true') + t.equal(isSameType(String, {}), false, 'reports false with String and object') + t.equal(isSameType(String, []), false, 'reports false with String and array') + + t.equal(isSameType(0, String), false, 'reports false with falsey number and String') + t.equal(isSameType(0, String), false, 'reports false with truthy number and String') + t.equal(isSameType(false, String), false, 'reports false with false and String') + t.equal(isSameType(true, String), false, 'reports false with true and String') + t.equal(isSameType({}, String), false, 'reports false with object and String') + t.equal(isSameType([], String), false, 'reports false with array and String') + + t.equal(isSameType('', false), false, 'reports false with falsey string and false') + t.equal(isSameType('', true), false, 'reports false with falsey string and true') + t.equal(isSameType('', {}), false, 'reports false with falsey string and object') + t.equal(isSameType('', []), false, 'reports false with falsey string and array') + t.equal(isSameType('', noop), false, 'reports false with falsey string and function') + + t.equal(isSameType(false, ''), false, 'reports false with false and falsey string') + t.equal(isSameType(true, ''), false, 'reports false with true and falsey string') + t.equal(isSameType({}, ''), false, 'reports false with object and falsey string') + t.equal(isSameType([], ''), false, 'reports false with array and falsey string') + t.equal(isSameType(noop, ''), false, 'reports false with function and falsey string') + + t.equal(isSameType('string', false), false, 'reports false with truthy string and false') + t.equal(isSameType('string', true), false, 'reports false with truthy string and true') + t.equal(isSameType('string', {}), false, 'reports false with truthy string and object') + t.equal(isSameType('string', []), false, 'reports false with truthy string and array') + t.equal(isSameType('string', noop), false, 'reports false with truthy string and function') + + t.equal(isSameType(false, 'string'), false, 'reports false with false and truthy string') + t.equal(isSameType(true, 'string'), false, 'reports false with true and truthy string') + t.equal(isSameType({}, 'string'), false, 'reports false with object and truthy string') + t.equal(isSameType([], 'string'), false, 'reports false with array and truthy string') + t.equal(isSameType(noop, 'string'), false, 'reports false with function and truthy string') + + t.end() +}) + +test('isSameType (Booleans)', t => { + t.equal(isSameType(Boolean, false), true, 'reports true with Boolean and false') + t.equal(isSameType(Boolean, true), true, 'reports true with Boolean and true') + t.equal(isSameType(false, Boolean), true, 'reports true with false and Boolean') + t.equal(isSameType(true, Boolean), true, 'reports true with true and Boolean') + + t.equal(isSameType(false, true), true, 'reports true with false and true') + t.equal(isSameType(true, false), true, 'reports true with true and false') + t.equal(isSameType(true, true), true, 'reports true with true and true') + t.equal(isSameType(false, false), true, 'reports true with false and false') + + t.equal(isSameType(Boolean, 0), false, 'reports false with Boolean and falsey number') + t.equal(isSameType(Boolean, 1), false, 'reports false with Boolean and truthy number') + t.equal(isSameType(Boolean, ''), false, 'reports false with Boolean and falsey string') + t.equal(isSameType(Boolean, 'string'), false, 'reports false with Boolean and truthy string') + t.equal(isSameType(Boolean, {}), false, 'reports false with Boolean and object') + t.equal(isSameType(Boolean, []), false, 'reports false with Boolean and array') + + t.equal(isSameType(0, Boolean), false, 'reports false with falsey number and Boolean') + t.equal(isSameType(1, Boolean), false, 'reports false with truthy number and Boolean') + t.equal(isSameType('', Boolean), false, 'reports false with falsey string and Boolean') + t.equal(isSameType('string', Boolean), false, 'reports false with truthy string and Boolean') + t.equal(isSameType({}, Boolean), false, 'reports false with object and Boolean') + t.equal(isSameType([], Boolean), false, 'reports false with array and Boolean') + + t.equal(isSameType(false, {}), false, 'reports false with false string and object') + t.equal(isSameType(false, []), false, 'reports false with false string and array') + t.equal(isSameType(false, noop), false, 'reports false with false string and function') + + t.equal(isSameType({}, false), false, 'reports false with object and false string') + t.equal(isSameType([], false), false, 'reports false with array and false string') + t.equal(isSameType(noop, false), false, 'reports false with function and false string') + + t.equal(isSameType(true, {}), false, 'reports false with true and object') + t.equal(isSameType(true, []), false, 'reports false with true and array') + t.equal(isSameType(true, noop), false, 'reports false with true and function') + + t.equal(isSameType({}, true), false, 'reports false with object and true') + t.equal(isSameType([], true), false, 'reports false with array and true') + t.equal(isSameType(noop, true), false, 'reports false with function and true') + + t.end() +}) + +test('isSameType (Objects)', t => { + t.equal(isSameType(Object, {}), true, 'reports true with Object and object') + t.equal(isSameType({}, Object), true, 'reports true with object and Object') + + t.equal(isSameType({}, {}), true, 'reports true with object and object') + + t.equal(isSameType(Object, 0), false, 'reports false with Object and falsey number') + t.equal(isSameType(Object, 1), false, 'reports false with Object and truthy number') + t.equal(isSameType(Object, ''), false, 'reports false with Object and falsey string') + t.equal(isSameType(Object, 'string'), false, 'reports false with Object and truthy string') + t.equal(isSameType(Object, false), false, 'reports false with Object and false') + t.equal(isSameType(Object, true), false, 'reports false with Object and true') + t.equal(isSameType(Object, []), false, 'reports false with Object and array') + + t.equal(isSameType(0, Object), false, 'reports false with falsey number and Object') + t.equal(isSameType(1, Object), false, 'reports false with truthy number and Object') + t.equal(isSameType('', Object), false, 'reports false with falsey string and Object') + t.equal(isSameType('string', Object), false, 'reports false with truthy string and Object') + t.equal(isSameType(false, Object), false, 'reports false with false and Object') + t.equal(isSameType(true, Object), false, 'reports false with true and Object') + t.equal(isSameType([], Object), false, 'reports false with array and Object') + + t.equal(isSameType({}, []), false, 'reports false with object and array') + t.equal(isSameType({}, noop), false, 'reports false with object and function') + + t.equal(isSameType([], {}), false, 'reports false with array and object') + t.equal(isSameType(noop, {}), false, 'reports false with noop and object') + + t.end() +}) + +test('isSameType (Arrays)', t => { + t.equal(isSameType(Array, []), true, 'reports true with Array and array') + t.equal(isSameType([], Array), true, 'reports true with array and Array') + + t.equal(isSameType([], []), true, 'reports true with array and array') + + t.equal(isSameType(Array, 0), false, 'reports false with Array and falsey number') + t.equal(isSameType(Array, 1), false, 'reports false with Array and truthy number') + t.equal(isSameType(Array, ''), false, 'reports false with Array and falsey string') + t.equal(isSameType(Array, 'string'), false, 'reports false with Array and truthy string') + t.equal(isSameType(Array, false), false, 'reports false with Array and false') + t.equal(isSameType(Array, true), false, 'reports false with Array and true') + t.equal(isSameType(Array, {}), false, 'reports false with Array and object') + + t.equal(isSameType(0, Array), false, 'reports false with falsey number and Array') + t.equal(isSameType(1, Array), false, 'reports false with truthy number and Array') + t.equal(isSameType('', Array), false, 'reports false with falsey string and Array') + t.equal(isSameType('string', Array), false, 'reports false with truthy string and Array') + t.equal(isSameType(false, Array), false, 'reports false with false and Array') + t.equal(isSameType(true, Array), false, 'reports false with true and Array') + t.equal(isSameType({}, Array), false, 'reports false with object and Array') + + t.equal(isSameType(noop, []), false, 'reports false with function and array') + t.equal(isSameType([], noop), false, 'reports false with array and function') + + t.end() +}) + +test('isSameType (Functions)', t => { + t.equal(isSameType(Function, noop), true, 'reports true with Function and function') + t.equal(isSameType(noop, Function), true, 'reports true with function and Function') + + t.equal(isSameType(noop, x => x), true, 'reports true with function and function') + + t.equal(isSameType(Function, 0), false, 'reports false with Function and falsey number') + t.equal(isSameType(Function, 1), false, 'reports false with Function and truthy number') + t.equal(isSameType(Function, ''), false, 'reports false with Function and falsey string') + t.equal(isSameType(Function, 'string'), false, 'reports false with Function and truthy string') + t.equal(isSameType(Function, false), false, 'reports false with Function and false') + t.equal(isSameType(Function, true), false, 'reports false with Function and true') + t.equal(isSameType(Function, {}), false, 'reports false with Function and object') + t.equal(isSameType(Function, []), false, 'reports false with Function and array') + + t.equal(isSameType(0, Function), false, 'reports false with falsey number and Function') + t.equal(isSameType(1, Function), false, 'reports false with truthy number and Function') + t.equal(isSameType('', Function), false, 'reports false with falsey string and Function') + t.equal(isSameType('string', Function), false, 'reports false with truthy string and Function') + t.equal(isSameType(false, Function), false, 'reports false with false string and Function') + t.equal(isSameType(true, Function), false, 'reports false with true and Function') + t.equal(isSameType({}, Function), false, 'reports false with object and Function') + t.equal(isSameType([], Function), false, 'reports false with array and Function') + + t.equal(isSameType(false, {}), false, 'reports false with false string and object') + t.equal(isSameType(false, []), false, 'reports false with false string and array') + + t.equal(isSameType({}, false), false, 'reports false with object and false string') + t.equal(isSameType([], false), false, 'reports false with array and false string') + + t.equal(isSameType(true, {}), false, 'reports false with true and object') + t.equal(isSameType(true, []), false, 'reports false with true and array') + + t.equal(isSameType({}, true), false, 'reports false with object and true') + t.equal(isSameType([], true), false, 'reports false with array and true') + + t.end() +})