UNPKG

xduce

Version:

Composable algorithmic transformations library for JavaScript

402 lines (375 loc) 15.8 kB
/* * Copyright (c) 2017 Thomas Otterson * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** * A set of functions related to the producing reducer objects, marking reduced objects, and performing general * reduction operations. * * @module reduction * @private */ const { isArray, isFunction, isObject, isString } = require('./util'); const { isKvFormObject, iterator } = require('./iteration'); const { protocols, isImplemented } = require('./protocol'); const p = protocols; /** * Returns an init function for a collection. This is a function that returns a new, empty instance of the collection in * question. If the collection doesn't support reduction, `null` is returned. This makes conditionals a bit easier to * work with. * * In order to support the conversion of functions into reducers, function support is also provided. * * @private * * @param {*} collection A collection to create an init function for. This can be anything that supports the ES2015 * iteration protocol, a plain object, a pre-ES2015 string or array, or a function. * @return {module:xduce~init} A function that, when called, returns an initial version of the provided collection. If * the provided collection is not iterable, then `null` is returned. */ function init(collection) { switch (true) { case isImplemented(collection, 'init'): return collection[p.init]; case isString(collection): return () => ''; case isArray(collection): return () => []; case isObject(collection): return () => ({}); case isFunction(collection): return () => { throw Error('init not available'); }; default: return null; } } /** * Returns a step function for a collection. This is a function that takes an accumulator and a value and returns the * result of reducing the value into the accumulator. If the collection doesn't support reduction, `null` is returned. * The returned function itself simply reduces the input into the target collection without modifying it. * * In order to support the conversion of functions into reducers, function support is also provided. * * @private * * @param {*} collection A collection to create a step function for. This can be anything that supports the ES2015 * iteration protocol, a plain object, a pre-ES2015 string or array, or a function. * @return {module:xduce~step} A reduction function for the provided collection that simply adds an element to the * target collection without modifying it. If the provided collection is not iterable, `null` is returned. */ function step(collection) { switch (true) { case isImplemented(collection, 'step'): return collection[p.step]; case isString(collection): return (acc, input) => { const value = isKvFormObject(input) ? input.v : input; return acc + value; }; case isArray(collection): return (acc, input) => { const value = isKvFormObject(input) ? { [input.k]: input.v } : input; acc.push(value); return acc; }; case isObject(collection): return (acc, input) => { let value = input; if (isKvFormObject(input)) { // if the object is kv-form, change the object from { k: key, v: value } to { key: value } value = { [input.k]: input.v }; } else if (!isObject(input)) { // if the input isn't an object at all, turn it into an object with a key based on what's already in the // accumulator let max = -1; for (const k1 in acc) { const knum = parseInt(k1); if (knum > max) { max = knum; } } value = { [max + 1]: input }; } for (const k2 in value) { if (value.hasOwnProperty(k2)) { acc[k2] = value[k2]; } } return acc; }; case isFunction(collection): return (acc, input) => collection(acc, input); default: return null; } } /** * Returns a result function for a collection. This is a function that performs any final processing that should be done * on the result of a reduction. If the collection doesn't support reduction, `null` is returned. * * In order to support the conversion of functions into reducers, function support is also provided. * * @private * * @param {*} collection A collection to create a step function for. This can be anything that supports the ES2015 * iteration protocol, a plain object, a pre-ES2015 string or array, or a function. * @return {module:xduce~result} A function that, when given a reduced collection, produces the final output. If the * provided collection is not iterable, `null` will be returned. */ function result(collection) { switch (true) { case isImplemented(collection, 'result'): return collection[p.result]; case isString(collection): case isArray(collection): case isObject(collection): case isFunction(collection): return value => value; default: return null; } } /** * **Creates a reducer object from a function or from a built-in reducible type (array, object, or string).** * * To create a reducer for arrays, objects, or strings, simply pass an empty version of that collection to this function * (e.g., `toReducer([])`). These reducers support the kv-form for objects. * * The notable use for this function though is to turn a reduction function into a reducer object. The function is a * function oftwo parameters, an accumulator and a value, and returns the accumulator with the value in it. This is * exactly the same kind of function that is passed to reduction functions like JavaScript's `Array.prototype.reduce` * and Lodash's `_.reduce`. * * Note in particular that the output of this reducer does not need to be a collection. It can be anything. While * transducing normally involves transforming one collection into another, it need not be so. For example, here is a * reducer that will result in summing of the collection values. * * ``` * const { toReducer, reduce } = xduce; * * const sumReducer = toReducer((acc, input) => acc + input); * const sum = reduce([1, 2, 3, 4, 5], sumReducer, 0); * // sum = 15 * ``` * * This can be combined with transducers as well, as in this calculation of the sum of the *squares* of the collection * values. * * ``` * const { toReducer, transduce } = xduce; * const { map } = xduce.transducers; * * const sumReducer = toReducer((acc, input) => acc + input); * const sum = transduce([1, 2, 3, 4, 5], map(x => x * x), sumReducer, 0); * // sum = 55 * ``` * * @memberof module:xduce * * @param {*} collection An iterable collection or a reducer function. * @return {object} An object containing protocol properties for init, step, and result. This object is suitable for * use as a reducer object (one provided to `{@link xduce.reduce|reduce}` or `{@link xduce.transduce|transduce}`). * If the provided collection is not iterable, all of the properties of this object will be `null`. */ function toReducer(collection) { return { [p.init]: init(collection), [p.step]: step(collection), [p.result]: result(collection) }; } // Reducer functions for the three common built-in iterable types. const arrayReducer = toReducer([]); const objectReducer = toReducer({}); const stringReducer = toReducer(''); /** * **Creates a reduction function from a transducer and a reducer.** * * This produces a function that's suitable for being passed into other libraries' reduce functions, such as * JavaScript's `Array.prototype.reduce` or Lodash's `_.reduce`. It requires both a transformer and a reducer because * reduction functions for those libraries must know how to do both. The reducer can be a standard reducer object like * the ones sent to`{@link module:xduce.transduce|transduce}` or `{@link module:xduce.reduce|reduce}`, or it can be a * plain function that takes two parameters and returns the result of reducing the second parameter into the first. * * If there is no need for a transformation, then pass in the `{@link module:xduce.identity|identity}` transducer. * * @memberof module:xduce * * @param {module:xduce~transducerObject} xform A transducer object whose step function will become the returned * reduction function. * @param {(module:xduce~step|module:xduce~transducerObject)} reducer A reducer that knows how to reduce values into an * output collection. This can either be a reducing function or a transducer object whose `step` function knows how * to perform this reduction. * @return {module:xduce~step} A function that handles both the transformation and the reduction of a value onto a * target function. */ function toFunction(xform, reducer) { const r = typeof reducer === 'function' ? toReducer(reducer) : reducer; const result = xform(r); return result[p.step].bind(result); } /** * **Marks a value as complete.** * * This is done by wrapping the value. This means three things: first, a complete object may be marked as complete * again; second, a complete value isn't usable without being uncompleted first; and third any type of value (including * `undefined`) may be marked as complete. * * @memberof module:xduce.util.status * * @param {*} value The value to be completed. * @return {*} A completed version of the provided value. This reduction is achieved by wrapping the value in a marker * object. */ function complete(value) { return { [p.reduced]: true, [p.value]: value }; } /** * **Removes the complete status from a completed value.** * * This function is intended to be used when it's certain that a value is already marked as complete. If it is not, * `undefined` will be returned instead of the value. * * @memberof module:xduce.util.status * * @param {*} value The value to be uncompleted. * @return {*} An uncompleted version of the provided value. If the value was not complete in the first place, * `undefined` will be returned instead. */ function uncomplete(value) { if (value == null) { return; } return value[p.value]; } /** * **Determines whether a value is marked as complete.** * * @memberof module:xduce.util.status * * @param {*} value The value to test for its complete status. * @return {boolean} Either `true` if the value is complete, or `false` if it is not. */ function isCompleted(value) { if (value == null) { return false; } return !!value[p.reduced]; } /** * **Makes sure that a value is marked as complete; if it is not, it will be marked as complete.** * * This differs from {@link module:xduce.util.status.complete|complete} in that if the value is already complete, this * function won't complete it again. Therefore thus function can't be used to make a value complete multiple times. * * @memberof module:xduce.util.status * * @param {*} value The value to be completed. * @return {*} If the value is already complete, then the value is simply returned. Otherwise, a completed version of * the value is returned. */ function ensureCompleted(value) { return isCompleted(value) ? value : complete(value); } /** * **Removes the complete status from a value, as long as it actually is complete.** * * This does a check to make sure the value passed in actually is complete. If it isn't, the value itself is returned. * It's meant to be used when the completed status is uncertain. * * @memberof module:xduce.util.status * * @param {*} value The complete value to be uncompleted. * @return {*} If the value is already uncompleted, the value is simply returned. Otherwise an uncompleted version of * the value is returned. */ function ensureUncompleted(value) { return isCompleted(value) ? uncomplete(value) : value; } /** * **Reduces the elements of the input collection through a reducer into an output collection.** * * This is the lowest-level of the transduction functions. In fact, this one is so low-level that it doesn't have a lot * of use in normal operation. It's more useful for writing your own transformation functions. * * `reduce` doesn't assume that there's even a transformation. It requires an initial collection and a reducer object * that is matched to that initial collection. The reducer object must implement the `step` and `result` protocols, * which instruct `reduce` on how to build up the collection. The reducer may implement a transformation as well, but * all that's important here is that it can do the reduction. * * The input collection need only implement `iterator`. It is not necessary for the input and output collections to be * of the same type; as long as the input implements `iterator` and the reducer implements `step` and `result` * appropriate to the type of the `init` collection, then any translation between collection types can occur. * * The normal course of operation will be to call {@link module:xduce.transduce|transduce} instead, as that function * makes it easy to combine transformations with reductions and can optionally figure out the initial collection itself. * * @memberof module:xduce * * @param {*} collection The input collection. The only requirement of this collection is that it implement the * `iterator` protocol. Special support is provided by the library for objects and pre-ES2015 arrays and strings * (ES2015 arrays and strings already implement `iterator`), so any of those can also be used. * @param {object} reducer An object that implements the `step` and `result` protocols. This object must know how to * produce an output collection through those protocol functions. * @param {*} init a collection of the same type as the output collection. It need not be empty; if it is not, the * existing elements are retained as the input collection is reduced into it. * @return {*} A new collection, consisting of the `init` collection with all of the elements of the `collection` * collection reduced into it. */ function reduce(collection, reducer, init) { if (collection == null) { return null; } const iter = iterator(collection, null, true); if (!iter) { throw Error(`Cannot reduce an instance of ${collection.constructor.name}`); } let acc = init; let step = iter.next(); while (!step.done) { acc = reducer[p.step](acc, step.value); if (isCompleted(acc)) { acc = uncomplete(acc); break; } step = iter.next(); } return reducer[p.result](acc); } module.exports = { init, step, result, toReducer, arrayReducer, objectReducer, stringReducer, toFunction, complete, uncomplete, isCompleted, ensureCompleted, ensureUncompleted, reduce };