From 6f6a86e8e18f2b74406a74b0092c5803bdb347ea Mon Sep 17 00:00:00 2001 From: "Ian Hofmann-Hicks (evil)" Date: Fri, 19 May 2017 23:17:47 -0700 Subject: [PATCH] move Star and Arrow from Semigroups to Semigroupoids --- README.md | 86 ++++++++++++++++++++++++++++--- crocks.js | 4 ++ crocks.spec.js | 8 +++ crocks/Arrow.js | 33 ++++++------ crocks/Arrow.spec.js | 36 ++++++------- crocks/Star.js | 8 +-- crocks/Star.spec.js | 20 +++---- helpers/compose.js | 1 + helpers/composeK.js | 1 + helpers/composeK.spec.js | 2 - helpers/composeS.js | 39 ++++++++++++++ helpers/composeS.spec.js | 69 +++++++++++++++++++++++++ helpers/fanout.js | 2 +- helpers/pipeK.spec.js | 2 - helpers/pipeS.js | 39 ++++++++++++++ helpers/pipeS.spec.js | 69 +++++++++++++++++++++++++ predicates/isCategory.js | 13 +++++ predicates/isCategory.spec.js | 31 +++++++++++ predicates/isSemigroupoid.js | 11 ++++ predicates/isSemigroupoid.spec.js | 28 ++++++++++ 20 files changed, 440 insertions(+), 62 deletions(-) create mode 100644 helpers/composeS.js create mode 100644 helpers/composeS.spec.js create mode 100644 helpers/pipeS.js create mode 100644 helpers/pipeS.spec.js create mode 100644 predicates/isCategory.js create mode 100644 predicates/isCategory.spec.js create mode 100644 predicates/isSemigroupoid.js create mode 100644 predicates/isSemigroupoid.spec.js diff --git a/README.md b/README.md index 4619ea535..be90e6ad0 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ All `Crocks` are Constructor functions of the given type, with `Writer` being an | Crock | Constructor | Instance | |---|:---|:---| -| `Arrow` | `empty` | `both`, `concat`, `contramap`, `empty`, `first`, `map`, `promap`, `runWith`, `second`, `value` | +| `Arrow` | `id` | `both`, `compose`, `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`, `concat`, `either`, `equals`, `map`, `of`, `sequence`, `swap`, `traverse` | @@ -85,7 +85,7 @@ All `Crocks` are Constructor functions of the given type, with `Writer` being an | `Pred` * | `empty` | `concat`, `contramap`, `empty`, `runWith`, `value` | | `Reader` | `ask`, `of`| `ap`, `chain`, `map`, `of`, `runWith` | | `Result` | `Err`, `Ok`, `of`| `alt`, `ap`, `bimap`, `chain`, `coalesce`, `concat`, `either`, `equals`, `map`, `of`, `sequence`, `swap`, `traverse` | -| `Star` | -- | `both`, `concat`, `contramap`, `map`, `promap`, `runWith` | +| `Star` | -- | `both`, `compose`, `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` | @@ -264,6 +264,38 @@ const promFunc = ``` Due to the nature of the `then` function, only the head of your composition needs to return a `Promise`. This will create a function that takes a value, which is passed through your chain, returning a `Promise` which can be extended. This is only a `then` chain, it does not do anything with the `catch` function. If you would like to provide your functions in a left-to-right manner, check out [pipeP](#pipep). +#### composeS +```haskell +composeS : Semigroupoid s => (s y z, s x y, ..., s a b) -> s a z +``` +When working with things like `Arrow` and `Star` there will come a point when you would like to compose them like you would any `Function`. That is where `composeS` comes in handy. Just pass it the `Semigroupoid`s you want to compose and it will give you back a new `Semigroupoid` of the same type with all of the underlying functions composed and ready to be run. Like [`compose`](#compose), `composeS` composes the functions in a right-to-left fashion. If you would like to represent your flow in a more left-to-right manner, then [`pipeS`](#pipeS) is provided for such things. + +```js +const { + Arrow, bimap, branch, composeS, merge, mreduce, Sum +} = require('crocks') + +const length = + xs => xs.length + +const divide = + (x, y) => x / y + +const avg = + Arrow(bimap(mreduce(Sum), length)) + .promap(branch, merge(divide)) + +const double = + Arrow(x => x * 2) + +const data = + [ 34, 198, 3, 43, 92 ] + +composeS(double, avg) + .runWith(data) +// => 148 +``` + #### curry ```haskell curry : ((a, b, ...) -> z) -> a -> b -> ... -> z @@ -366,7 +398,7 @@ const data = [ 13, 5, 13 ] map(max10, data) -// [ 10, 5, 10] +// => [ 10, 5, 10] ``` #### pick @@ -383,7 +415,7 @@ If you find yourself not able to come to terms with doing the typical right-to-l #### pipeK ```haskell -pipe : Chain m => ((a -> m b), (b -> m c), ..., (y -> m z)) -> a -> m z +pipeK : Chain m => ((a -> m b), (b -> m c), ..., (y -> m z)) -> a -> m z ``` Like [`composeK`](#composek), you can remove much of the boilerplate when chaining together a series of functions with the signature: `Chain m => a -> m b`. The difference between the two functions is, while [`composeK`](composek) is right-to-left, `pipeK` is the opposite, taking its functions left-to-right. @@ -436,6 +468,44 @@ const promPipe = pipeP(proimse, doSomething, doAnother) ``` +#### pipeS +```haskell +pipeS : Semigroupoid s => (s a b, s b c, ..., s y z) -> s a z +``` +While `Star`s and `Arrow`s come in very handy at times, the only thing that could make them better is to compose them . With `pipeS` you can do just that with any `Semigroupoid`. Just like with [`composeS`](#composes), you just pass it `Semigroupoid`s of the same type and you will get back another `Semigroupoid` with them all composed together. The only difference between the two, is that `pipeS` composes in a left-to-right fashion, while [`composeS`](#composes) does the opposite. + +```js +const { + curry, isNumber, pipeS, prop, safeLift, Star +} = require('../crocks') + +const add = curry( + (x, y) => x + y +) + +const pull = + x => Star(prop(x)) + +const safeAdd = + x => Star(safeLift(isNumber, add(x))) + +const data = { + num: 56, + string: '56' +} + +const flow = (key, num) => pipeS( + pull(key), + safeAdd(num) +) + +flow('num', 10).runWith(data) +// => Just 66 + +flow('string', 100).runWith(data) +// => Nothing +``` + #### prop ```haskell prop : (String | Number) -> (Object | Array) -> Maybe a @@ -538,8 +608,9 @@ All functions in this group have a signature of `* -> Boolean` and are used with * `isApply : a -> Boolean`: an ADT that provides `map` and `ap` functions * `isArray : a -> Boolean`: Array * `isBoolean : a -> Boolean`: Boolean -* `isDefined : a -> Boolean`: Every value that is not `undefined`, `null` included +* `isCategory : a -> Boolean`: an ADT that provides `id` and `compose` functions * `isChain : a -> Boolean`: an ADT that provides `map`, `ap` and `chain` functions +* `isDefined : a -> Boolean`: Every value that is not `undefined`, `null` included * `isEmpty : a -> Boolean`: Empty Object, Array or String * `isFoldable : a -> Boolean`: Array, List or any structure with a `reduce` function * `isFunction : a -> Boolean`: Function @@ -553,6 +624,7 @@ All functions in this group have a signature of `* -> Boolean` and are used with * `isPromise : a -> Boolean`: An object implementing `then` and `catch` * `isSameType : a -> b -> Boolean`: Constructor matches a values type, or two values types match * `isSemigroup : a -> Boolean`: an ADT that provides a `concat` function +* `isSemigroupoid : a -> Boolean`: an ADT that provides a `compose` function * `isSetoid : a -> Boolean`: an ADT that provides an `equals` function * `isString : a -> Boolean`: String * `isTraversable : a -> Boolean`: an ADT that provides `map` and `traverse` functions @@ -635,11 +707,11 @@ These functions provide a very clean way to build out very simple functions and | `both` | `Arrow`, `Function`, `Star` | | `chain` | `Array`, `Async`, `Const`, `Either`, `Identity`, `IO`, `List`, `Maybe`, `Pair`, `Reader`, `Result`, `State`, `Unit`, `Writer` | | `coalesce` | `Async`, `Either`, `Maybe`, `Result` | -| `concat` | `All`, `Any`, `Array`, `Arrow`, `Assign`, `Const`, `Either`, `Endo`, `Identity`, `List`, `Max`, `Maybe`, `Min`, `Pair`, `Pred`, `Prod`, `Result`, `Star`, `String`, `Sum`, `Unit` | +| `concat` | `All`, `Any`, `Array`, `Assign`, `Const`, `Either`, `Endo`, `Identity`, `List`, `Max`, `Maybe`, `Min`, `Pair`, `Pred`, `Prod`, `Result`, `String`, `Sum`, `Unit` | | `cons` | `Array`, `List` | | `contramap` | `Arrow`, `Pred`, `Star` | | `either` | `Either`, `Maybe`, `Result` | -| `empty` | `All`, `Any`, `Array`, `Assign`,, `Endo`, `List`, `Max`, `Min`, `Object`, `Pred`, `Prod`, `String`, `Sum`, `Unit` | +| `empty` | `All`, `Any`, `Array`, `Assign`, `Endo`, `List`, `Max`, `Min`, `Object`, `Pred`, `Prod`, `String`, `Sum`, `Unit` | | `evalWith` | `State` | | `execWith` | `State` | | `filter` | `Array`, `List`, `Object` | diff --git a/crocks.js b/crocks.js index 520a59183..6f63fad32 100644 --- a/crocks.js +++ b/crocks.js @@ -38,6 +38,7 @@ const helpers = { compose: require('./helpers/compose'), composeK: require('./helpers/composeK'), composeP: require('./helpers/composeP'), + composeS: require('./helpers/composeS'), curry: require('./helpers/curry'), defaultProps: require('./helpers/defaultProps'), defaultTo: require('./helpers/defaultTo'), @@ -59,6 +60,7 @@ const helpers = { pipe: require('./helpers/pipe'), pipeK: require('./helpers/pipeK'), pipeP: require('./helpers/pipeP'), + pipeS: require('./helpers/pipeS'), prop: require('./helpers/prop'), propPath: require('./helpers/propPath'), safe: require('./helpers/safe'), @@ -134,6 +136,7 @@ const predicates = { isApply: require('./predicates/isApply'), isArray: require('./predicates/isArray'), isBoolean: require('./predicates/isBoolean'), + isCategory: require('./predicates/isCategory'), isChain: require('./predicates/isChain'), isDefined: require('./predicates/isDefined'), isEmpty: require('./predicates/isEmpty'), @@ -150,6 +153,7 @@ const predicates = { isSameType: require('./predicates/isSameType'), isSetoid: require('./predicates/isSetoid'), isSemigroup: require('./predicates/isSemigroup'), + isSemigroupoid: require('./predicates/isSemigroupoid'), isString: require('./predicates/isString'), isTraversable: require('./predicates/isTraversable') } diff --git a/crocks.spec.js b/crocks.spec.js index b2705c489..c773a8841 100644 --- a/crocks.spec.js +++ b/crocks.spec.js @@ -17,6 +17,7 @@ const branch = require('./helpers/branch') const compose = require('./helpers/compose') const composeK = require('./helpers/composeK') const composeP = require('./helpers/composeP') +const composeS = require('./helpers/composeS') const curry = require('./helpers/curry') const defaultProps = require('./helpers/defaultProps') const defaultTo = require('./helpers/defaultTo') @@ -38,6 +39,7 @@ const pick = require('./helpers/pick') const pipe = require('./helpers/pipe') const pipeK = require('./helpers/pipeK') const pipeP = require('./helpers/pipeP') +const pipeS = require('./helpers/pipeS') const prop = require('./helpers/prop') const propPath = require('./helpers/propPath') const safe = require('./helpers/safe') @@ -123,6 +125,7 @@ const isApplicative = require('./predicates/isApplicative') const isApply = require('./predicates/isApply') const isArray = require('./predicates/isArray') const isBoolean = require('./predicates/isBoolean') +const isCategory = require('./predicates/isCategory') const isChain = require('./predicates/isChain') const isDefined = require('./predicates/isDefined') const isEmpty = require('./predicates/isEmpty') @@ -138,6 +141,7 @@ const isObject = require('./predicates/isObject') const isPromise = require('./predicates/isPromise') const isSameType = require('./predicates/isSameType') const isSemigroup = require('./predicates/isSemigroup') +const isSemigroupoid = require('./predicates/isSemigroupoid') const isSetoid = require('./predicates/isSetoid') const isString = require('./predicates/isString') const isTraversable = require('./predicates/isTraversable') @@ -172,6 +176,7 @@ test('entry', t => { t.equal(crocks.compose, compose, 'provides the compose function') t.equal(crocks.composeK, composeK, 'provides the composeK function') t.equal(crocks.composeP, composeP, 'provides the composeP function') + t.equal(crocks.composeS, composeS, 'provides the composeS function') t.equal(crocks.curry, curry, 'provides the curry function') t.equal(crocks.defaultProps, defaultProps, 'provides the defaultProps function') t.equal(crocks.defaultTo, defaultTo, 'provides the defaultTo function') @@ -193,6 +198,7 @@ test('entry', t => { t.equal(crocks.pipe, pipe, 'provides the pipe function') t.equal(crocks.pipeK, pipeK, 'provides the pipeK function') t.equal(crocks.pipeP, pipeP, 'provides the pipeP function') + t.equal(crocks.pipeS, pipeS, 'provides the pipeS function') t.equal(crocks.prop, prop, 'provides the prop function') t.equal(crocks.propPath, propPath, 'provides the propPath function') t.equal(crocks.safe, safe, 'provides the safe function') @@ -278,6 +284,7 @@ test('entry', t => { t.equal(crocks.isApply, isApply, 'provides the isApply function') t.equal(crocks.isArray, isArray, 'provides the isArray function') t.equal(crocks.isBoolean, isBoolean, 'provides the isBoolean function') + t.equal(crocks.isCategory, isCategory, 'provides the isCategory function') t.equal(crocks.isChain, isChain, 'provides the isChain function') t.equal(crocks.isDefined, isDefined, 'provides the isDefined function') t.equal(crocks.isEmpty, isEmpty, 'provides the isEmpty function') @@ -293,6 +300,7 @@ test('entry', t => { t.equal(crocks.isPromise, isPromise, 'provides the isPromise function') t.equal(crocks.isSameType, isSameType, 'provides the isSameType function') t.equal(crocks.isSemigroup, isSemigroup, 'provides the isSemigroup function') + t.equal(crocks.isSemigroupoid, isSemigroupoid, 'provides the isSemigroupoid function') t.equal(crocks.isSetoid, isSetoid, 'provides the isSetoid function') t.equal(crocks.isString, isString, 'provides the isString function') t.equal(crocks.isTraversable, isTraversable, 'provides the isTraversable function') diff --git a/crocks/Arrow.js b/crocks/Arrow.js index 0ca8b0554..ee2852e3c 100644 --- a/crocks/Arrow.js +++ b/crocks/Arrow.js @@ -1,22 +1,19 @@ /** @license ISC License (c) copyright 2016 original and current authors */ /** @author Ian Hofmann-Hicks (evil) */ -const isFunction = require('../predicates/isFunction') - -const isSameType = require('../predicates/isSameType') const _inspect = require('../internal/inspect') - -const compose = require('../helpers/compose') - -const identity = require('../combinators/identity') +const composeB = require('../combinators/composeB') const constant = require('../combinators/constant') +const identity = require('../combinators/identity') +const isFunction = require('../predicates/isFunction') +const isSameType = require('../predicates/isSameType') const Pair = require('./Pair') const _type = constant('Arrow') -const _empty = +const _id = () => Arrow(identity) function Arrow(runWith) { @@ -27,18 +24,18 @@ function Arrow(runWith) { const type = _type - const empty = - _empty - const value = constant(runWith) const inspect = constant(`Arrow${_inspect(value())}`) - function concat(m) { + const id = + _id + + function compose(m) { if(!(isSameType(Arrow, m))) { - throw new TypeError('Arrow.concat: Arrow required') + throw new TypeError('Arrow.compose: Arrow required') } return map(m.runWith) @@ -49,7 +46,7 @@ function Arrow(runWith) { throw new TypeError('Arrow.map: Function required') } - return Arrow(compose(fn, runWith)) + return Arrow(composeB(fn, runWith)) } function contramap(fn) { @@ -57,7 +54,7 @@ function Arrow(runWith) { throw new TypeError('Arrow.contramap: Function required') } - return Arrow(compose(runWith, fn)) + return Arrow(composeB(runWith, fn)) } function promap(l, r) { @@ -65,7 +62,7 @@ function Arrow(runWith) { throw new TypeError('Arrow.promap: Functions required for both arguments') } - return Arrow(compose(r, runWith, l)) + return Arrow(composeB(r, composeB(runWith, l))) } function first() { @@ -98,12 +95,12 @@ function Arrow(runWith) { return { inspect, type, value, runWith, - concat, empty, map, contramap, + id, compose, map, contramap, promap, first, second, both } } -Arrow.empty = _empty +Arrow.id = _id Arrow.type = _type module.exports = Arrow diff --git a/crocks/Arrow.spec.js b/crocks/Arrow.spec.js index c9f8a65f8..64a46a923 100644 --- a/crocks/Arrow.spec.js +++ b/crocks/Arrow.spec.js @@ -20,8 +20,8 @@ test('Arrow', t => { t.ok(isFunction(Arrow), 'is a function') - t.ok(isFunction(Arrow.empty), 'provides an empty function') t.ok(isFunction(Arrow.type), 'provides a type function') + t.ok(isFunction(Arrow.id), 'provides an id function') t.ok(isObject(Arrow(unit)), 'returns an object') @@ -85,7 +85,7 @@ test('Arrow runWith', t => { t.end() }) -test('Arrow concat functionality', t => { +test('Arrow compose functionality', t => { const f = x => x + 1 const g = x => x * 0 @@ -97,7 +97,7 @@ test('Arrow concat functionality', t => { const notArrow = { type: constant('Arrow...Not') } - const cat = bindFunc(a.concat) + const cat = bindFunc(a.compose) t.throws(cat(undefined), TypeError, 'throws with undefined') t.throws(cat(null), TypeError, 'throws with null') @@ -111,48 +111,48 @@ test('Arrow concat functionality', t => { t.throws(cat({}), TypeError, 'throws with an object') t.throws(cat(notArrow), TypeError, 'throws with non-Arrow') - t.same(a.concat(b).runWith(x), result, 'builds composition as expected') + t.same(a.compose(b).runWith(x), result, 'builds composition as expected') t.end() }) -test('Arrow concat properties (Semigroup)', t => { +test('Arrow compose properties (Semigroupoid)', 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') + t.ok(isFunction(Arrow(identity).compose), 'is a function') - const left = a.concat(b).concat(c).runWith - const right = a.concat(b.concat(c)).runWith + const left = a.compose(b).compose(c).runWith + const right = a.compose(b.compose(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.same(a.compose(b).type(), a.type(), 'returns Semigroupoid of same type') t.end() }) -test('Arrow empty functionality', t => { - const m = Arrow(unit).empty() +test('Arrow id functionality', t => { + const m = Arrow(unit).id() - t.equal(m.empty, Arrow.empty, 'static and instance versions are the same') + t.equal(m.id, Arrow.id, 'static and instance versions are the same') - t.equal(m.type(), 'Arrow', 'provides an Arrow') + t.equal(m.type(), Arrow.type(), 'provides an Arrow') t.same(m.runWith(13), 13, 'wraps an identity function') t.end() }) -test('Arrow empty properties (Monoid)', t => { +test('Arrow id properties (Category)', 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') + t.ok(isFunction(m.compose), 'provides a compose function') + t.ok(isFunction(m.id), 'provides an id function') - const right = m.concat(m.empty()).runWith - const left = m.empty().concat(m).runWith + const right = m.compose(m.id()).runWith + const left = m.id().compose(m).runWith t.same(right(x), m.runWith(x), 'right identity') t.same(left(x), m.runWith(x), 'left identity') diff --git a/crocks/Star.js b/crocks/Star.js index 2868df6d9..febcdbe10 100644 --- a/crocks/Star.js +++ b/crocks/Star.js @@ -8,7 +8,7 @@ const isSameType = require('../predicates/isSameType') const _inspect = require('../internal/inspect') -const compose = require('../helpers/compose') +const composeB = require('../combinators/composeB') const constant = require('../combinators/constant') const merge = require('../pointfree/merge') @@ -30,7 +30,7 @@ function Star(runWith) { const inspect = constant(`Star${_inspect(runWith)}`) - function concat(s) { + function compose(s) { if(!isSameType(Star, s)) { throw new TypeError('Star.concat: Star required') } @@ -75,7 +75,7 @@ function Star(runWith) { throw new TypeError('Star.contramap: Function required') } - return Star(compose(runWith, fn)) + return Star(composeB(runWith, fn)) } function promap(l, r) { @@ -144,7 +144,7 @@ function Star(runWith) { } return { - inspect, type, runWith, concat, map, + inspect, type, runWith, compose, map, contramap, promap, first, second, both } } diff --git a/crocks/Star.spec.js b/crocks/Star.spec.js index dc051f6c4..2c6499a12 100644 --- a/crocks/Star.spec.js +++ b/crocks/Star.spec.js @@ -76,7 +76,7 @@ test('Star runWith', t => { t.end() }) -test('Star concat functionality', t => { +test('Star compose functionality', t => { const f = x => MockCrock(x + 1) const a = Star(f) @@ -84,7 +84,7 @@ test('Star concat functionality', t => { const notStar = { type: constant('Star...Not') } const notMock = { type: constant('Mock...Not') } - const cat = bindFunc(a.concat) + const cat = bindFunc(a.compose) t.throws(cat(undefined), TypeError, 'throws with undefined') t.throws(cat(null), TypeError, 'throws with null') @@ -98,7 +98,7 @@ test('Star concat functionality', t => { t.throws(cat({}), TypeError, 'throws with an object') t.throws(cat(notStar), TypeError, 'throws with non-Star') - const noMonadFst = bindFunc(Star(identity).concat(a).runWith) + const noMonadFst = bindFunc(Star(identity).compose(a).runWith) t.throws(noMonadFst(undefined), TypeError, 'throws when first computation returns undefined') t.throws(noMonadFst(null), TypeError, 'throws when first computation returns null') @@ -111,7 +111,7 @@ test('Star concat functionality', t => { t.throws(noMonadFst({}), TypeError, 'throws when first computation returns false') t.throws(noMonadFst([]), TypeError, 'throws when first computation returns true') - const noMonadSnd = bindFunc(x => a.concat(Star(constant(x))).runWith(10)) + const noMonadSnd = bindFunc(x => a.compose(Star(constant(x))).runWith(10)) t.throws(noMonadSnd(undefined), TypeError, 'throws when second computation returns undefined') t.throws(noMonadSnd(null), TypeError, 'throws when second computation returns null') @@ -129,26 +129,26 @@ test('Star concat functionality', t => { const g = x => MockCrock(x * 10) const chained = f(x).chain(g).value() - const star = a.concat(Star(g)).runWith(x).value() + const star = a.compose(Star(g)).runWith(x).value() t.equal(chained, star, 'builds composition as expected') t.end() }) -test('Star concat properties (Semigroup)', t => { +test('Star compose properties (Semigroupoid)', t => { const a = Star(x => MockCrock(x + 1)) const b = Star(x => MockCrock(x * 10)) const c = Star(x => MockCrock(x - 5)) - t.ok(isFunction(Star(identity).concat), 'is a function') + t.ok(isFunction(Star(identity).compose), 'is a function') - const left = a.concat(b).concat(c).runWith - const right = a.concat(b.concat(c)).runWith + const left = a.compose(b).compose(c).runWith + const right = a.compose(b.compose(c)).runWith const x = 20 t.same(left(x).value(), right(x).value(), 'associativity') - t.same(a.concat(b).type(), a.type(), 'returns Semigroup of same type') + t.same(a.compose(b).type(), a.type(), 'returns Semigroupoid of same type') t.end() }) diff --git a/helpers/compose.js b/helpers/compose.js index b6ef021dd..304adf8a6 100644 --- a/helpers/compose.js +++ b/helpers/compose.js @@ -16,6 +16,7 @@ function applyPipe(f, g) { return g.call(null, f.apply(null, argsArray(arguments))) } } + // compose : ((y -> z), (x -> y), ..., (a -> b)) -> a -> z function compose() { if(!arguments.length) { diff --git a/helpers/composeK.js b/helpers/composeK.js index fd16fc40f..d9e3e939c 100644 --- a/helpers/composeK.js +++ b/helpers/composeK.js @@ -7,6 +7,7 @@ const isFunction = require('../predicates/isFunction') const err = 'composeK: Chain returning functions of the same type required' +// composeK : Chain m => ((y -> m z), (x -> m y), ..., (a -> m b)) -> a -> m z function composeK() { if(!(arguments.length)) { throw new TypeError(err) diff --git a/helpers/composeK.spec.js b/helpers/composeK.spec.js index 71a2ac6ef..3f51429b7 100644 --- a/helpers/composeK.spec.js +++ b/helpers/composeK.spec.js @@ -82,7 +82,5 @@ test('composeK function', t => { t.ok(f.calledWith(23, 30), 'applies all arguments to head function') t.equal(f.lastCall.returnValue, resSingle, 'returns the result of the function') - t.equals() - t.end() }) diff --git a/helpers/composeS.js b/helpers/composeS.js new file mode 100644 index 000000000..a0ca1c17b --- /dev/null +++ b/helpers/composeS.js @@ -0,0 +1,39 @@ +/** @license ISC License (c) copyright 2017 original and current authors */ +/** @author Ian Hofmann-Hicks (evil) */ + +const err = 'composeS: Semigroupoids of the same type required' + +const argsArray = require('../internal/argsArray') +const isSameType = require('../predicates/isSameType') +const isSemigroupoid = require('../predicates/isSemigroupoid') + +// composeS : Semigroupoid s => (s y z, s x y, ..., s a b) -> s a z +function composeS() { + if(!(arguments.length)) { + throw new TypeError(err) + } + + const ms = + argsArray(arguments).slice().reverse() + + const head = + ms[0] + + if(!isSemigroupoid(head)) { + throw new TypeError(err) + } + + if(ms.length === 1) { + return head + } + + return ms.slice().reduce((comp, m) => { + if(!isSameType(comp, m)) { + throw new TypeError(err) + } + + return comp.compose(m) + }) +} + +module.exports = composeS diff --git a/helpers/composeS.spec.js b/helpers/composeS.spec.js new file mode 100644 index 000000000..a61812711 --- /dev/null +++ b/helpers/composeS.spec.js @@ -0,0 +1,69 @@ +const test = require('tape') +const sinon = require('sinon') +const helpers = require('../test/helpers') + +const bindFunc = helpers.bindFunc + +const constant = require('../combinators/constant') +const identity = require('../combinators/identity') + +const composeS = require('./composeS') + +const Mock = x => ({ + compose: sinon.spy(identity), + type: constant('Mock'), + value: constant(x) +}) + +test('composeS parameters', t => { + const c = bindFunc(composeS) + + const err = /composeS: Semigroupoids of the same type required/ + t.throws(composeS, err, 'throws when nothing passed') + + t.throws(c(undefined, Mock(0)), err, 'throws when undefined passed first') + t.throws(c(null, Mock(0)), err, 'throws when null passed passed first') + t.throws(c('', Mock(0)), err, 'throws when falsey string passed first') + t.throws(c('string', Mock(0)), err, 'throws when truthy string passed first') + t.throws(c(0, Mock(0)), err, 'throws when falsy number passed first') + t.throws(c(1, Mock(0)), err, 'throws when truthy number passed first') + t.throws(c(false, Mock(0)), err, 'throws when false passed first') + t.throws(c(true, Mock(0)), err, 'throws when true passed first') + t.throws(c({}, Mock(0)), err, 'throws when object passed first') + t.throws(c([], Mock(0)), err, 'throws when array passed first') + t.throws(c(identity, Mock(0)), err, 'throws when function passed first') + + t.throws(c(Mock(0), undefined), err, 'throws when undefined passed after first') + t.throws(c(Mock(0), null), err, 'throws when null passed passed after first') + t.throws(c(Mock(0), ''), err, 'throws when falsey string passed after first') + t.throws(c(Mock(0), 'string'), err, 'throws when truthy string passed after first') + t.throws(c(Mock(0), 0), err, 'throws when falsy number passed after first') + t.throws(c(Mock(0), 1), err, 'throws when truthy number passed after first') + t.throws(c(Mock(0), false), err, 'throws when false passed after first') + t.throws(c(Mock(0), true), err, 'throws when true passed after first') + t.throws(c(Mock(0), {}), err, 'throws when object passed after first') + t.throws(c(Mock(0), []), err, 'throws when array passed after first') + t.throws(c(Mock(0), identity), err, 'throws when function passed after first') + + t.end() +}) + +test('composeS function', t => { + const f = Mock('a') + const g = Mock('b') + const h = Mock('c') + + const m = composeS(f, g, h) + + t.ok(h.compose.calledWith(g), 'calls compose on the last (head) passing the previous') + t.ok(g.compose.calledWith(f), 'calls compose on the penultimate passing the first') + t.equals(g.compose.lastCall.returnValue, m, 'returns the result of compose on the penultimate argument') + + f.compose.reset() + + const single = composeS(f) + + t.equals(single, f, 'returns the semigroupoid when only one is passed in') + + t.end() +}) diff --git a/helpers/fanout.js b/helpers/fanout.js index 7e28ed910..756ad7857 100644 --- a/helpers/fanout.js +++ b/helpers/fanout.js @@ -25,7 +25,7 @@ function fanout(fst, snd) { if(valid(fst, snd)) { return first(fst) - .concat(second(snd)) + .compose(second(snd)) .contramap(branch) } diff --git a/helpers/pipeK.spec.js b/helpers/pipeK.spec.js index a34f122a4..7b231ab36 100644 --- a/helpers/pipeK.spec.js +++ b/helpers/pipeK.spec.js @@ -82,7 +82,5 @@ test('pipeK function', t => { t.ok(f.calledWith(23, 30), 'applies all arguments to head function') t.equal(f.lastCall.returnValue, resSingle, 'returns the result of the function') - t.equals() - t.end() }) diff --git a/helpers/pipeS.js b/helpers/pipeS.js new file mode 100644 index 000000000..d6760f2f8 --- /dev/null +++ b/helpers/pipeS.js @@ -0,0 +1,39 @@ +/** @license ISC License (c) copyright 2017 original and current authors */ +/** @author Ian Hofmann-Hicks (evil) */ + +const err = 'pipeS: Semigroupoids of the same type required' + +const argsArray = require('../internal/argsArray') +const isSameType = require('../predicates/isSameType') +const isSemigroupoid = require('../predicates/isSemigroupoid') + +// pipeS : Semigroupoid s => (s a b, s b c, ..., s y z) -> s a z +function pipeS() { + if(!(arguments.length)) { + throw new TypeError(err) + } + + const ms = + argsArray(arguments).slice() + + const head = + ms[0] + + if(!isSemigroupoid(head)) { + throw new TypeError(err) + } + + if(ms.length === 1) { + return head + } + + return ms.slice().reduce((comp, m) => { + if(!isSameType(comp, m)) { + throw new TypeError(err) + } + + return comp.compose(m) + }) +} + +module.exports = pipeS diff --git a/helpers/pipeS.spec.js b/helpers/pipeS.spec.js new file mode 100644 index 000000000..98025e898 --- /dev/null +++ b/helpers/pipeS.spec.js @@ -0,0 +1,69 @@ +const test = require('tape') +const sinon = require('sinon') +const helpers = require('../test/helpers') + +const bindFunc = helpers.bindFunc + +const constant = require('../combinators/constant') +const identity = require('../combinators/identity') + +const pipeS = require('./pipeS') + +const Mock = x => ({ + compose: sinon.spy(identity), + type: constant('Mock'), + value: constant(x) +}) + +test('pipeS parameters', t => { + const c = bindFunc(pipeS) + + const err = /pipeS: Semigroupoids of the same type required/ + t.throws(pipeS, err, 'throws when nothing passed') + + t.throws(c(undefined, Mock(0)), err, 'throws when undefined passed first') + t.throws(c(null, Mock(0)), err, 'throws when null passed passed first') + t.throws(c('', Mock(0)), err, 'throws when falsey string passed first') + t.throws(c('string', Mock(0)), err, 'throws when truthy string passed first') + t.throws(c(0, Mock(0)), err, 'throws when falsy number passed first') + t.throws(c(1, Mock(0)), err, 'throws when truthy number passed first') + t.throws(c(false, Mock(0)), err, 'throws when false passed first') + t.throws(c(true, Mock(0)), err, 'throws when true passed first') + t.throws(c({}, Mock(0)), err, 'throws when object passed first') + t.throws(c([], Mock(0)), err, 'throws when array passed first') + t.throws(c(identity, Mock(0)), err, 'throws when function passed first') + + t.throws(c(Mock(0), undefined), err, 'throws when undefined passed after first') + t.throws(c(Mock(0), null), err, 'throws when null passed passed after first') + t.throws(c(Mock(0), ''), err, 'throws when falsey string passed after first') + t.throws(c(Mock(0), 'string'), err, 'throws when truthy string passed after first') + t.throws(c(Mock(0), 0), err, 'throws when falsy number passed after first') + t.throws(c(Mock(0), 1), err, 'throws when truthy number passed after first') + t.throws(c(Mock(0), false), err, 'throws when false passed after first') + t.throws(c(Mock(0), true), err, 'throws when true passed after first') + t.throws(c(Mock(0), {}), err, 'throws when object passed after first') + t.throws(c(Mock(0), []), err, 'throws when array passed after first') + t.throws(c(Mock(0), identity), err, 'throws when function passed after first') + + t.end() +}) + +test('pipeS function', t => { + const f = Mock('a') + const g = Mock('b') + const h = Mock('c') + + const m = pipeS(f, g, h) + + t.ok(f.compose.calledWith(g), 'calls compose on the first (head) passing the next') + t.ok(g.compose.calledWith(h), 'calls compose on the penultimate passing the last') + t.equals(g.compose.lastCall.returnValue, m, 'returns the result of compose on the penultimate argument') + + f.compose.reset() + + const single = pipeS(f) + + t.equals(single, f, 'returns the semigroupoid when only one is passed in') + + t.end() +}) diff --git a/predicates/isCategory.js b/predicates/isCategory.js new file mode 100644 index 000000000..6d7c66f62 --- /dev/null +++ b/predicates/isCategory.js @@ -0,0 +1,13 @@ +/** @license ISC License (c) copyright 2017 original and current authors */ +/** @author Ian Hofmann-Hicks (evil) */ + +const isFunction = require('./isFunction') + +// isCategory : a -> Boolean +function isCategory(m) { + return !!m + && isFunction(m.compose) + && isFunction(m.id) +} + +module.exports = isCategory diff --git a/predicates/isCategory.spec.js b/predicates/isCategory.spec.js new file mode 100644 index 000000000..ca5852aec --- /dev/null +++ b/predicates/isCategory.spec.js @@ -0,0 +1,31 @@ +const test = require('tape') + +const identity = require('../combinators/identity') +const isFunction = require('./isFunction') + +const isCategory = require('./isCategory') + +test('isCategory predicate function', t => { + const fake = { + compose: identity, + id: identity + } + + t.ok(isFunction(isCategory)) + + t.equal(isCategory(undefined), false, 'returns false for undefined') + t.equal(isCategory(null), false, 'returns false for null') + t.equal(isCategory(0), false, 'returns false for falsey number') + t.equal(isCategory(1), false, 'returns false for truthy number') + t.equal(isCategory(''), false, 'returns false for falsey string') + t.equal(isCategory('string'), false, 'returns false for truthy string') + t.equal(isCategory(false), false, 'returns false for false') + t.equal(isCategory(true), false, 'returns false for true') + t.equal(isCategory([]), false, 'returns false for an array') + t.equal(isCategory({}), false, 'returns false for an object') + t.equal(isCategory(identity), false, 'returns false for function') + + t.equal(isCategory(fake), true, 'returns true when a Semigroupoid is passed') + + t.end() +}) diff --git a/predicates/isSemigroupoid.js b/predicates/isSemigroupoid.js new file mode 100644 index 000000000..56afb3dab --- /dev/null +++ b/predicates/isSemigroupoid.js @@ -0,0 +1,11 @@ +/** @license ISC License (c) copyright 2017 original and current authors */ +/** @author Ian Hofmann-Hicks (evil) */ + +const isFunction = require('./isFunction') + +// isSemigroupoid : a -> Boolean +function isSemigroupoid(m) { + return !!m && isFunction(m.compose) +} + +module.exports = isSemigroupoid diff --git a/predicates/isSemigroupoid.spec.js b/predicates/isSemigroupoid.spec.js new file mode 100644 index 000000000..5c2698f1a --- /dev/null +++ b/predicates/isSemigroupoid.spec.js @@ -0,0 +1,28 @@ +const test = require('tape') + +const identity = require('../combinators/identity') +const isFunction = require('./isFunction') + +const isSemigroupoid = require('./isSemigroupoid') + +test('isSemigroupoid predicate function', t => { + const fake = { compose: identity } + + t.ok(isFunction(isSemigroupoid)) + + t.equal(isSemigroupoid(undefined), false, 'returns false for undefined') + t.equal(isSemigroupoid(null), false, 'returns false for null') + t.equal(isSemigroupoid(0), false, 'returns false for falsey number') + t.equal(isSemigroupoid(1), false, 'returns false for truthy number') + t.equal(isSemigroupoid(''), false, 'returns false for falsey string') + t.equal(isSemigroupoid('string'), false, 'returns false for truthy string') + t.equal(isSemigroupoid(false), false, 'returns false for false') + t.equal(isSemigroupoid(true), false, 'returns false for true') + t.equal(isSemigroupoid([]), false, 'returns false for an array') + t.equal(isSemigroupoid({}), false, 'returns false for an object') + t.equal(isSemigroupoid(identity), false, 'returns false for function') + + t.equal(isSemigroupoid(fake), true, 'returns true when a Semigroupoid is passed') + + t.end() +})