From b2f8a778d3fb8d247455effc12422cec7c1d3855 Mon Sep 17 00:00:00 2001 From: Dale Francis Date: Sun, 21 Jul 2019 12:35:24 -0700 Subject: [PATCH] Adding in the `compose2` combinator (#431) * Adding in the `compose2` combinator * Updates per review + minor doc fixes --- docs/src/pages/docs/crocks/Async.md | 12 +- docs/src/pages/docs/crocks/Reader.md | 4 +- docs/src/pages/docs/crocks/Tuple.md | 6 +- docs/src/pages/docs/functions/combinators.md | 128 ++++++++++++++++++- docs/src/pages/docs/functions/helpers.md | 22 ++-- src/combinators/compose2.js | 16 +++ src/combinators/compose2.spec.js | 59 +++++++++ src/combinators/index.js | 1 + src/combinators/index.spec.js | 2 + src/index.spec.js | 2 + 10 files changed, 228 insertions(+), 24 deletions(-) create mode 100644 src/combinators/compose2.js create mode 100644 src/combinators/compose2.spec.js diff --git a/docs/src/pages/docs/crocks/Async.md b/docs/src/pages/docs/crocks/Async.md index 7f705485b..8aee68dbd 100644 --- a/docs/src/pages/docs/crocks/Async.md +++ b/docs/src/pages/docs/crocks/Async.md @@ -1397,10 +1397,10 @@ logResult(timeout(fast)) //=> resolved: "All good" logResult(timeout(slow)) -// => rejected: "Error: Request has timed out" +//=> rejected: "Error: Request has timed out" logResult(failingPromise) -// => rejected: "Promise rejected!" +//=> rejected: "Promise rejected!" ``` #### eitherToAsync @@ -1593,7 +1593,7 @@ Resolved(First.empty()) Resolved(First(42)) .chain(firstToAsync('Left')) .fork(log('rej'), log('res')) -// => res: 42 +//=> res: 42 ``` #### lastToAsync @@ -1694,7 +1694,7 @@ Resolved(Last.empty()) Resolved(Last('too know!')) .chain(lastToAsync('Left')) .fork(log('rej'), log('res')) -// => res: "too know!" +//=> res: "too know!" ``` #### maybeToAsync @@ -1781,7 +1781,7 @@ Resolved(Nothing()) Resolved(Just('the 2 of us')) .chain(maybeToAsync('Left')) .fork(log('rej'), log('res')) -// => res: "the 2 of us" +//=> res: "the 2 of us" ``` #### resultToAsync @@ -1868,7 +1868,7 @@ Resolved(Err('Invalid entry')) Resolved(Ok('Success!')) .chain(resultToAsync) .fork(log('rej'), log('res')) -// => res: "Success!" +//=> res: "Success!" ``` diff --git a/docs/src/pages/docs/crocks/Reader.md b/docs/src/pages/docs/crocks/Reader.md index 569647e33..d2a342a17 100644 --- a/docs/src/pages/docs/crocks/Reader.md +++ b/docs/src/pages/docs/crocks/Reader.md @@ -46,11 +46,11 @@ const flow = flow .runWith('Thomas') -// => Hola, Thomas...See Ya Thomas +//=> Hola, Thomas...See Ya Thomas flow .runWith('Jenny') -// => Hola, Jenny...See Ya Jenny +//=> Hola, Jenny...See Ya Jenny ```
diff --git a/docs/src/pages/docs/crocks/Tuple.md b/docs/src/pages/docs/crocks/Tuple.md index 9404aad93..0dabf4cde 100644 --- a/docs/src/pages/docs/crocks/Tuple.md +++ b/docs/src/pages/docs/crocks/Tuple.md @@ -538,8 +538,10 @@ const Triple = Tuple(3) const triple = Triple( 1, { key: 'value' }, 'string' ) -tupleToArray(triple) // => [ 1, { key: 'value' }, 'string' ] +tupleToArray(triple) +//=> [ 1, { key: 'value' }, 'string' ] -tupleToArray(constant(triple))() // => [ 1, { key: 'value' }, 'string' ] +tupleToArray(constant(triple))() +//=> [ 1, { key: 'value' }, 'string' ] ```
diff --git a/docs/src/pages/docs/functions/combinators.md b/docs/src/pages/docs/functions/combinators.md index 94d96e9f7..65d5820ba 100644 --- a/docs/src/pages/docs/functions/combinators.md +++ b/docs/src/pages/docs/functions/combinators.md @@ -20,6 +20,125 @@ give it a value and it will give you back a function ready to take a function. Once that function is provided, it will return the result of applying your value to that function. +#### compose2 + +`crocks/combinators/compose2` + +```haskell +compose2 :: (c -> d -> e) -> (a -> c) -> (b -> d) -> a -> b -> e +``` + +`compose2` allows for composition between a `binary` function and +two `unary` functions. `compose2` takes a `binary` function followed by +two `unary` functions and returns a `binary` function that maps the first +argument with the first `unary` and the second with the second, passing +the results to the given `binary` and returning the result. + +```javascript +import compose2 from 'crocks/combinators/compose2' + +import and from 'crocks/logic/and' +import applyTo from 'crocks/combinators/applyTo' +import flip from 'crocks/combinators/flip' +import hasProp from 'crocks/predicates/hasProp' +import isNumber from 'crocks/predicates/isNumber' +import liftA2 from 'crocks/helpers/liftA2' +import map from 'crocks/pointfree/map' +import prop from 'crocks/Maybe/prop' +import safe from 'crocks/Maybe/safe' +import safeLift from 'crocks/Maybe/safeLift' + +// isNonZero :: Number -> Boolean +const isNonZero = x => + x !== 0 + +// isValidDivisor :: Number -> Boolean +const isValidDivisor = + and(isNumber, isNonZero) + +// divideBy :: Number -> Number -> Number +const divideBy = x => y => + y / x + +// safeDivide :: Number -> Number -> Maybe Number +const safeDivide = compose2( + liftA2(divideBy), + safe(isValidDivisor), + safe(isNumber) +) + +safeDivide(0.5, 21) +//=> Just 42 + +safeDivide('0.5', 21) +//=> Nothing + +safeDivide(0.5, '21') +//=> Nothing + +safeDivide(29, 0) +//=> Just 0 + +safeDivide(0, 29) +//=> Nothing + +// Item :: { id: Integer } +// Items :: Array Item +const items = + [ { id: 2 }, { id: 1 } ] + +// pluck :: String -> Array Object -> Maybe a +const pluck = + compose2(applyTo, prop, flip(map)) + +pluck('id', items) +//=> [ Just 2, Just 1 ] + +// summarize :: String -> String -> String +const summarize = name => count => + `${name} purchased ${count} items` + +// getLength :: a -> Maybe Number +const getLength = safeLift( + hasProp('length'), + x => x.length +) + +// createSummary :: Person -> Array Item -> String +const createSummary = compose2( + liftA2(summarize), + prop('name'), + getLength +) + +createSummary({ + name: 'Sam Smith' +}, items) +//=> Just "Sam Smith purchased 2 items" + +// capitalize :: String -> String +const capitalize = str => + `${str.charAt(0).toUpperCase()}${str.slice(1)}` + +// join :: String -> String -> String -> String +const join = delim => right => left => + `${left}${delim}${right}` + +// toUpper :: String -> String +const toUpper = x => + x.toUpperCase() + +// createName :: String -> String -> String +const createName = + compose2(join(', '), capitalize, toUpper) + +createName('Jon', 'doe') +//=> DOE, Jon + +createName('sara', 'smith') +//=> SMITH, Sara +``` + #### composeB `crocks/combinators/composeB` @@ -336,13 +455,16 @@ import liftA2 from 'crocks/helpers/liftA2' import safe from 'crocks/Maybe/safe' // isNonZero :: Number -> Boolean -const isNonZero = x => x !== 0 +const isNonZero = x => + x !== 0 // isValidDivisor :: Number -> Boolean -const isValidDivisor = and(isNumber, isNonZero) +const isValidDivisor = + and(isNumber, isNonZero) // divideBy :: Number -> Number -> Number -const divideBy = x => y => y / x +const divideBy = x => y => + y / x // safeDivide :: Number -> Number -> Maybe Number const safeDivide = diff --git a/docs/src/pages/docs/functions/helpers.md b/docs/src/pages/docs/functions/helpers.md index 3a8b51e5b..f718128a7 100644 --- a/docs/src/pages/docs/functions/helpers.md +++ b/docs/src/pages/docs/functions/helpers.md @@ -93,7 +93,7 @@ const fluent = x => .chain(getProp('mi')) fluent(data) -// => Just 'fa' +//=> Just 'fa' // pointfree :: a -> Maybe b const pointfree = compose( @@ -104,7 +104,7 @@ const pointfree = compose( ) pointfree(data) -// => Just 'fa' +//=> Just 'fa' ``` into the more abbreviated form: @@ -133,7 +133,7 @@ const flow = composeK( ) flow(data) -// => Just 'fa' +//=> Just 'fa' ``` As demonstrated in the above example, this function more closely resembles flows @@ -265,7 +265,7 @@ const data = composeS(double, avg) .runWith(data) -// => 148 +//=> 148 ``` #### curry @@ -651,7 +651,7 @@ const safeMax = mapReduce( safeMax(data) .option(Max.empty()) .valueOf() -// => 3 +//=> 3 ``` #### mconcat @@ -798,7 +798,7 @@ const data = [ 13, 5, 13 ] map(max10, data) -// => [ 10, 5, 10] +//=> [ 10, 5, 10] ``` #### pick @@ -874,7 +874,7 @@ const fluent = x => .chain(scaleLog(3)) fluent(0).log() -// => List [ "adding 4 to 0", "scaling 4 by 3" ] +//=> List [ "adding 4 to 0", "scaling 4 by 3" ] const chainPipe = pipeK( addLog(4), @@ -882,7 +882,7 @@ const chainPipe = pipeK( ) chainPipe(0).log() -// => List [ "adding 4 to 0", "scaling 4 by 3" ] +//=> List [ "adding 4 to 0", "scaling 4 by 3" ] ``` #### pipeP @@ -1009,11 +1009,11 @@ const flow = (key, num) => pipeS( flow('num', 10) .runWith(data) -// => Just 66 +//=> Just 66 flow('string', 100) .runWith(data) -// => Nothing +//=> Nothing ``` #### setPath @@ -1061,7 +1061,7 @@ setPath([ 'people', 2, 'age' ], 26, { // ] } setPath([ 'a', 'c' ], false, { a: { b: true } }) -// => { a: { b: true, c: false } } +//=> { a: { b: true, c: false } } setPath([ 'list', 'a' ], 'ohhh, I see.', { list: [ 'string', 'another' ] }) //=> { list: { 0: 'string', 1: 'another', a: 'ohhh, I see.' } } diff --git a/src/combinators/compose2.js b/src/combinators/compose2.js new file mode 100644 index 000000000..5d5480e7f --- /dev/null +++ b/src/combinators/compose2.js @@ -0,0 +1,16 @@ +/** @license ISC License (c) copyright 2019 original and current authors */ +/** @author Dale Francis (dalefrancis88) */ + +const curry = require('../core/curry') +const isFunction = require('../core/isFunction') + +// compose2 :: (c -> d -> e) -> (a -> c) -> (b -> d) -> a -> b -> e +function compose2(f, g, h, x, y) { + if(!isFunction(f) || !isFunction(g) || !isFunction(h)) { + throw new TypeError('compose2: First, second and third arguments must be functions') + } + + return curry(f)(g(x), h(y)) +} + +module.exports = curry(compose2) diff --git a/src/combinators/compose2.spec.js b/src/combinators/compose2.spec.js new file mode 100644 index 000000000..13d6f9bac --- /dev/null +++ b/src/combinators/compose2.spec.js @@ -0,0 +1,59 @@ +const test = require('tape') +const helpers = require('../test/helpers') + +const bindFunc = helpers.bindFunc + +const isFunction = require('../core/isFunction') + +const compose2 = require('./compose2') + +test('compose2', t => { + const fn = bindFunc(compose2) + const f = x => y => x * y + const g = x => x - 1 + const h = x => x + 1 + const x = 22 + const y = 1 + + t.ok(isFunction(compose2), 'is a function') + + const err = /^TypeError: compose2: First, second and third arguments must be functions/ + t.throws(fn(undefined, g, h, x, y), err, 'throws with first arg undefined') + t.throws(fn(null, g, h, x, y), err, 'throws with first arg null') + t.throws(fn(0, g, h, x, y), err, 'throws with first arg falsey number') + t.throws(fn(1, g, h, x, y), err, 'throws with first arg truthy number') + t.throws(fn('', g, h, x, y), err, 'throws with first arg falsey string') + t.throws(fn('string', g, h, x, y), err, 'throws with first arg truthy string') + t.throws(fn(false, g, h, x, y), err, 'throws with first arg false') + t.throws(fn(true, g, h, x, y), err, 'throws with first arg true') + t.throws(fn({}, g, h, x, y), err, 'throws with first arg an object') + t.throws(fn([], g, h, x, y), err, 'throws with first arg an array') + + t.throws(fn(f, undefined, h, x, y), err, 'throws with second arg undefined') + t.throws(fn(f, null, h, x, y), err, 'throws with second arg null') + t.throws(fn(f, 0, h, x, y), err, 'throws with second arg falsey number') + t.throws(fn(f, 1, h, x, y), err, 'throws with second arg truthy number') + t.throws(fn(f, '', h, x, y), err, 'throws with second arg falsey string') + t.throws(fn(f, 'bling', h, x, y), err, 'throws with second arg truthy string') + t.throws(fn(f, false, h, x, y), err, 'throws with second arg false') + t.throws(fn(f, true, h, x, y), err, 'throws with second arg true') + t.throws(fn(f, {}, h, x, y), err, 'throws with second arg an object') + t.throws(fn(f, [], h, x, y), err, 'throws with second arg an array') + + t.throws(fn(f, g, undefined, x, y), err, 'throws with third arg undefined') + t.throws(fn(f, g, null, x, y), err, 'throws with third arg null') + t.throws(fn(f, g, 0, x, y), err, 'throws with third arg falsey number') + t.throws(fn(f, g, 1, x, y), err, 'throws with third arg truthy number') + t.throws(fn(f, g, '', x, y), err, 'throws with third arg falsey string') + t.throws(fn(f, g, 'string', x, y), err, 'throws with third arg truthy string') + t.throws(fn(f, g, false, x, y), err, 'throws with third arg false') + t.throws(fn(f, g, true, x, y), err, 'throws with third arg true') + t.throws(fn(f, g, {}, x, y), err, 'throws with third arg an object') + t.throws(fn(f, g, [], x, y), err, 'throws with third arg an array') + + const result = fn(f, g, h, x, y) + + t.equal(result(), 42, 'returns expected result') + + t.end() +}) diff --git a/src/combinators/index.js b/src/combinators/index.js index 16425d521..bb453dca1 100644 --- a/src/combinators/index.js +++ b/src/combinators/index.js @@ -1,5 +1,6 @@ module.exports = { applyTo: require('./applyTo'), + compose2: require('./compose2'), composeB: require('./composeB'), constant: require('./constant'), converge: require('./converge'), diff --git a/src/combinators/index.spec.js b/src/combinators/index.spec.js index ddac981b6..7218b1fe3 100644 --- a/src/combinators/index.spec.js +++ b/src/combinators/index.spec.js @@ -3,6 +3,7 @@ const test = require('tape') const index = require('.') const applyTo = require('./applyTo') +const compose2 = require('./compose2') const composeB = require('./composeB') const constant = require('./constant') const converge = require('./converge') @@ -13,6 +14,7 @@ const substitution = require('./substitution') test('combinators entry', t => { t.equal(index.applyTo, applyTo, 'provides the T combinator (applyTo)') + t.equal(index.compose2, compose2, 'provides the compose2 combinator') t.equal(index.composeB, composeB, 'provides the B combinator (composeB)') t.equal(index.constant, constant, 'provides the K combinator (constant)') t.equal(index.converge, converge, 'provides the S\' combinator (converge)') diff --git a/src/index.spec.js b/src/index.spec.js index 0cad5e60a..974054806 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -3,6 +3,7 @@ const crocks = require('./index') // combinators const applyTo = require('./combinators/applyTo') +const compose2 = require('./combinators/compose2') const composeB = require('./combinators/composeB') const constant = require('./combinators/constant') const flip = require('./combinators/flip') @@ -243,6 +244,7 @@ test('entry', t => { // combinators t.equal(crocks.applyTo, applyTo, 'provides the T combinator (applyTo)') + t.equal(crocks.compose2, compose2, 'provides the compose2 combinator') t.equal(crocks.composeB, composeB, 'provides the B combinator (composeB)') t.equal(crocks.constant, constant, 'provides the K combinator (constant)') t.equal(crocks.flip, flip, 'provides the C combinator (flip)')