diff --git a/docs/src/pages/docs/crocks/index.md b/docs/src/pages/docs/crocks/index.md index b629b1eb8..91488724f 100644 --- a/docs/src/pages/docs/crocks/index.md +++ b/docs/src/pages/docs/crocks/index.md @@ -24,7 +24,7 @@ but what they do from type to type may vary. | [`Equiv`][equiv] | [`empty`][equiv-empty] | [`concat`][equiv-concat], [`contramap`][equiv-contra], [`compareWith`][equiv-compare], [`valueOf`][equiv-value] | | `Identity` | `of` | `ap`, `chain`, `concat`, `equals`, `map`, `of`, `sequence`, `traverse`, `valueOf` | | `IO` | `of` | `ap`, `chain`, `map`, `of`, `run` | -| `List` | `empty`, `fromArray`, `of` | `ap`, `chain`, `concat`, `cons`, `empty`, `equals`, `filter`, `head`, `map`, `of`, `reduce`, `reject`, `sequence`, `tail`, `toArray`, `traverse`, `valueOf` | +| `List` | `empty`, `fromArray`, `of` | `ap`, `chain`, `concat`, `cons`, `empty`, `equals`, `filter`, `head`, `map`, `of`, `reduce`, `reduceRight`, `reject`, `sequence`, `tail`, `toArray`, `traverse`, `valueOf` | | [`Maybe`][maybe] | [`Nothing`][maybe-nothing], [`Just`][maybe-just], [`of`][maybe-of], [`zero`][maybe-zero] | [`alt`][maybe-alt], [`ap`][maybe-ap], [`chain`][maybe-chain], [`coalesce`][maybe-coalesce], [`concat`][maybe-concat], [`equals`][maybe-equals], [`either`][maybe-either], [`map`][maybe-map], [`of`][maybe-of], [`option`][maybe-option], [`sequence`][maybe-sequence], [`traverse`][maybe-traverse], [`zero`][maybe-zero] | | `Pair` | --- | `ap`, `bimap`, `chain`, `concat`, `equals`, `extend`, `fst`, `map`, `merge`, `of`, `snd`, `swap`, `toArray` | | [`Pred`][pred] * | [`empty`][pred-empty] | [`concat`][pred-concat], [`contramap`][pred-contra], [`runWith`][pred-run], [`valueOf`][pred-value] | diff --git a/docs/src/pages/docs/functions/pointfree-functions.md b/docs/src/pages/docs/functions/pointfree-functions.md index a68b17b7e..35e3dc810 100644 --- a/docs/src/pages/docs/functions/pointfree-functions.md +++ b/docs/src/pages/docs/functions/pointfree-functions.md @@ -76,6 +76,7 @@ accepted Datatype): | `promap` | `(c -> a) -> (b -> d) -> m a b -> m c d` | `crocks/pointfree` | | `read` | `m a b -> Pair a b` | `crocks/Writer` | | `reduce` | `(b -> a -> b) -> b -> m a -> b` | `crocks/pointfree` | +| `reduceRight` | `(b -> a -> b) -> b -> m a -> b` | `crocks/pointfree` | | `reject` | ((a -> Boolean) | Pred a) -> m a -> m a | `crocks/pointfree` | | `run` | `m a -> b` | `crocks/pointfree` | | `runWith` | `a -> m -> b` | `crocks/pointfree` | @@ -117,6 +118,7 @@ accepted Datatype): | `promap` | [`Arrow`][arrow-pro], `Star` | | `read` | `Writer` | | `reduce` | `Array`, `List` | +| `reduceRight` | `Array`, `List` | | `reject` | `Array`, `List`, `Object` | | `run` | `IO` | | `runWith` | [`Arrow`][arrow-run], [`Endo`][endo-run], [`Pred`][pred-run], [`Reader`][reader-run], `Star`, [`State`][state-run] | diff --git a/src/core/List.js b/src/core/List.js index ed1429f69..dfeb8542f 100644 --- a/src/core/List.js +++ b/src/core/List.js @@ -22,8 +22,8 @@ const predOrFunc = require('./predOrFunc') const not = fn => x => !fn(x) -const _concat = - x => m => m.concat(x) +const _prepend = + x => m => x.concat(m) const { Nothing, Just } = require('./Maybe') @@ -44,11 +44,11 @@ function fromArray(xs) { function applyTraverse(x, y) { if(isArray(x)) { - return array.ap(x, array.map(v => _concat(List.of(v)), y)) + return array.ap(x, array.map(v => _prepend(List.of(v)), y)) } return y - .map(v => _concat(List.of(v))) + .map(v => _prepend(List.of(v))) .ap(x) } @@ -139,6 +139,14 @@ function List(x) { return xs.reduce(fn, i) } + function reduceRight(fn, i) { + if(!isFunction(fn)) { + throw new TypeError('List.reduceRight: Function required for first argument') + } + + return xs.reduceRight(fn, i) + } + function fold() { if(isEmpty(xs)) { throw new TypeError('List.fold: List must contain at least one Semigroup') @@ -216,7 +224,7 @@ function List(x) { throw new TypeError('List.sequence: Applicative Function required') } - return reduce( + return reduceRight( runSequence, af(List.empty()) ) @@ -227,7 +235,7 @@ function List(x) { throw new TypeError('List.traverse: Applicative returning functions required for both arguments') } - return reduce( + return reduceRight( runTraverse(f), af(List.empty()) ) @@ -236,8 +244,8 @@ function List(x) { return { inspect, toString: inspect, valueOf, toArray, head, tail, cons, type, equals, concat, empty, - reduce, fold, filter, reject, map, ap, of, chain, - sequence, traverse, + reduce, reduceRight, fold, filter, reject, map, + ap, of, chain, sequence, traverse, '@@type': _type, constructor: List } diff --git a/src/core/List.spec.js b/src/core/List.spec.js index 1b804bbea..58357f86a 100644 --- a/src/core/List.spec.js +++ b/src/core/List.spec.js @@ -319,6 +319,33 @@ test('List reduce functionality', t => { t.end() }) +test('List reduceRight errors', t => { + const reduce = bindFunc(List([ 1, 2 ]).reduceRight) + + const err = /List.reduceRight: Function required for first argument/ + t.throws(reduce(undefined, 0), err, 'throws with undefined in the first argument') + t.throws(reduce(null, 0), err, 'throws with null in the first argument') + t.throws(reduce(0, 0), err, 'throws with falsey number in the first argument') + t.throws(reduce(1, 0), err, 'throws with truthy number in the first argument') + t.throws(reduce('', 0), err, 'throws with falsey string in the first argument') + t.throws(reduce('string', 0), err, 'throws with truthy string in the first argument') + t.throws(reduce(false, 0), err, 'throws with false in the first argument') + t.throws(reduce(true, 0), err, 'throws with true in the first argument') + t.throws(reduce({}, 0), err, 'throws with an object in the first argument') + t.throws(reduce([], 0), err, 'throws with an array in the first argument') + + t.end() +}) + +test('List reduceRight functionality', t => { + const f = (y, x) => y.concat(x) + const m = List([ '1', '2', '3' ]) + + t.equal(m.reduceRight(f, '4'), '4321', 'reduces as expected') + + t.end() +}) + test('List fold errors', t => { const f = bindFunc(x => List(x).fold()) diff --git a/src/core/array.js b/src/core/array.js index 6ca4b59b2..7a604fc48 100644 --- a/src/core/array.js +++ b/src/core/array.js @@ -9,7 +9,7 @@ const isSameType = require('./isSameType') const identity = x => x const concat = - x => m => m.concat(x) + x => m => x.concat(m) function runTraverse(name, fn) { return function(acc, x) { @@ -56,11 +56,11 @@ function chain(f, m) { } function sequence(af, m) { - return m.reduce(runTraverse('sequence', identity), af([])) + return m.reduceRight(runTraverse('sequence', identity), af([])) } function traverse(af, fn, m) { - return m.reduce(runTraverse('traverse', fn), af([])) + return m.reduceRight(runTraverse('traverse', fn), af([])) } module.exports = { diff --git a/src/index.js b/src/index.js index 96f65cc48..b4818bb5e 100644 --- a/src/index.js +++ b/src/index.js @@ -131,6 +131,7 @@ const pointfree = { promap: require('./pointfree/promap'), read: require('./Writer/read'), reduce: require('./pointfree/reduce'), + reduceRight: require('./pointfree/reduceRight'), reject: require('./pointfree/reject'), run: require('./pointfree/run'), runWith: require('./pointfree/runWith'), diff --git a/src/index.spec.js b/src/index.spec.js index 4967724ce..519ca8684 100644 --- a/src/index.spec.js +++ b/src/index.spec.js @@ -125,6 +125,7 @@ const option = require('./pointfree/option') const promap = require('./pointfree/promap') const read = require('./Writer/read') const reduce = require('./pointfree/reduce') +const reduceRight = require('./pointfree/reduceRight') const reject = require('./pointfree/reject') const run = require('./pointfree/run') const runWith = require('./pointfree/runWith') @@ -328,6 +329,7 @@ test('entry', t => { t.equal(crocks.promap, promap, 'provides the promap pointfree') t.equal(crocks.read, read, 'provides the read pointfree') t.equal(crocks.reduce, reduce, 'provides the reduce pointfree') + t.equal(crocks.reduceRight, reduceRight, 'provides the reduceRight pointfree') t.equal(crocks.reject, reject, 'provides the reject pointfree') t.equal(crocks.run, run, 'provides the run pointfree') t.equal(crocks.runWith, runWith, 'provides the runWith pointfree') diff --git a/src/pointfree/reduceRight.js b/src/pointfree/reduceRight.js new file mode 100644 index 000000000..f9fb71431 --- /dev/null +++ b/src/pointfree/reduceRight.js @@ -0,0 +1,18 @@ +/** @license ISC License (c) copyright 2017 original and current authors */ +/** @author Ian Hofmann-Hicks (evil) */ + +const curry = require('../core/curry') +const isFunction = require('../core/isFunction') + +function reduceRight(fn, init, m) { + if(!isFunction(fn)) { + throw new TypeError('reduceRight: Function required for first argument') + } + else if(!(m && isFunction(m.reduceRight))) { + throw new TypeError('reduceRight: Right Foldable required for third argument') + } + + return m.reduceRight(fn, init) +} + +module.exports = curry(reduceRight) diff --git a/src/pointfree/reduceRight.spec.js b/src/pointfree/reduceRight.spec.js new file mode 100644 index 000000000..163c1bc72 --- /dev/null +++ b/src/pointfree/reduceRight.spec.js @@ -0,0 +1,54 @@ +const test = require('tape') +const sinon = require('sinon') +const helpers = require('../test/helpers') + +const bindFunc = helpers.bindFunc + +const isFunction = require('../core/isFunction') +const unit = require('../core/_unit') + +const constant = x => () => x + +const reduceRight = require('./reduceRight') + +test('reduce pointfree', t => { + const r = bindFunc(reduceRight) + const x = 'result' + const m = { reduceRight: sinon.spy(constant(x)) } + + t.ok(isFunction(reduceRight), 'is a function') + + const noFunc = /reduceRight: Function required for first argument/ + t.throws(r(undefined, 0, m), noFunc, 'throws if first arg is undefined') + t.throws(r(null, 0, m), noFunc, 'throws if first arg is null') + t.throws(r(0, 0, m), noFunc, 'throws if first arg is a falsey number') + t.throws(r(1, 0, m), noFunc, 'throws if first arg is a truthy number') + t.throws(r('', 0, m), noFunc, 'throws if first arg is a falsey string') + t.throws(r('string', 0, m), noFunc, 'throws if first arg is a truthy string') + t.throws(r(false, 0, m), noFunc, 'throws if first arg is false') + t.throws(r(true, 0, m), noFunc, 'throws if first arg is true') + t.throws(r([], 0, m), noFunc, 'throws if first arg is an array') + t.throws(r({}, 0, m), noFunc, 'throws if first arg is an object') + + const err = /reduceRight: Right Foldable required for third argument/ + t.throws(r(unit, 0, undefined), err, 'throws if second arg is undefined') + t.throws(r(unit, 0, null), err, 'throws if second arg is null') + t.throws(r(unit, 0, 0), err, 'throws if second arg is a falsey number') + t.throws(r(unit, 0, 1), err, 'throws if second arg is a truthy number') + t.throws(r(unit, 0, ''), err, 'throws if second arg is a falsey string') + t.throws(r(unit, 0, 'string'), err, 'throws if second arg is a truthy string') + t.throws(r(unit, 0, false), err, 'throws if second arg is false') + t.throws(r(unit, 0, true), err, 'throws if second arg is true') + t.throws(r(unit, 0, {}), err, 'throws if second arg is an object') + + t.doesNotThrow(r(unit, 0, m), 'allows a function and Foldable') + t.doesNotThrow(r(unit, 0, []), 'allows a function and an array (Foldable)') + + const f = sinon.spy() + const res = reduceRight(f, 0, m) + + t.ok(m.reduceRight.calledWith(f), 'calls reduceRight on container, passing the function') + t.equal(res, x, 'returns the result of reduce') + + t.end() +})