@constantiner/fun-ctional
Version:
The library brings most of the familiar functional techniques (like functional composition) to asynchronous world with shining Promises
486 lines (456 loc) • 23.3 kB
JavaScript
/**
* @constantiner/fun-ctional
* The library brings most of the familiar functional techniques (like functional composition) to asynchronous world with shining Promises
*
* @author Konstantin Kovalev <constantiner@gmail.com>
* @version v0.6.6
* @link https://github.com/Constantiner/fun-ctional#readme
* @date 30 May 2019
*
* MIT License
*
* Copyright (c) 2018-2019 Konstantin Kovalev
*
* 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.
*
*/
const customPromiseHandlingSupportSupport = Symbol.for("Custom Promise Handling Support");
const addCustomPromiseHandlingSupport = (fn, customVersion) => (
(fn[customPromiseHandlingSupportSupport] = customVersion), fn
);
const supportsCustomPromiseHandling = fn => fn[customPromiseHandlingSupportSupport];
/**
* Composable version of catch method for promises.
*
* It gets a value (a promise or not), resolves it and if resulting promise was rejected calls catch function.
*
* It allows to handle errors within acompose or apipe asynchronous composition chains to restore broken state etc.
*
* A sample with acompose:
*
* <pre><code>const resultOrFallback = await acompose(acatch(handleAndRecoverFn), canFailFn)(someInput);</code></pre>
*
* Standalone usage:
*
* <pre><code>const resultOrFallback = await acatch(handleAndRecoverFn)(requestDataAndReturnPromise());</code></pre>
*
* It is the same as the following:
*
* <pre><code>requestDataAndReturnPromise().catch(handleAndRecoverFn).then(resultOrFallback => console.log(resultOrFallback));</code></pre>
*
* @param {function} catchFn Is function to handle Promise's rejection.
* @returns {any => Promise} A function which expects any value as input (Promise or not) and returns a Promise.
*/
var acatch = catchFn => {
const handler = value => Promise.resolve(value).catch(catchFn);
return addCustomPromiseHandlingSupport(handler, promise => promise.catch(catchFn));
};
/**
* Checks if parameter is an iterable.
*
* @param {any} obj is target to check.
* @returns {boolean} Result of checking.
*/
const isIterable = obj => Array.isArray(obj) || ((obj || obj === "") && typeof obj[Symbol.iterator] === "function");
/**
* Returns an array of values from arguments of some function.
* Arguments can be passed as arguments list ar as single iterable parameter.
*
* @param {array} args are input array (just arguments of some other function).
* If if consists of one element and this element is an iterable returns array from it.
* @returns {Promise} Resulting array of arguments.
*/
const extractResolvedArguments = args => (args ? (args.length === 1 && isIterable(args[0]) ? [...args[0]] : args) : []);
/**
* Asynchronous compose function (acompose stays for async-compose).
*
* The main purpose is to replace a Promise handling code like this:
* <pre><code>somePromise.then(normalize).then(upperCase).then(insertGreetings);</code></pre>
*
* with point-free style of functional compose syntax like the following:
* <pre><code>acompose(insertGreetings, upperCase, normalize)(somePromise);</code></pre>
*
* It is lazy and allows of reusing of promise handling chains.
*
* You can run acompose with Promise instance (for true asynchronous execution)
* or with any other object to use as usual functional composition.
* It produces a Promise and can be used in async/await context:
*
* <pre><code>const message = await acompose(insertGreetings, upperCase, normalize)(somePromise);</code></pre>
*
* It also allows to handle errors like for traditional Promise but only in the tail position of the chain:
*
* <pre><code>acompose(insertGreetings, upperCase, normalize)(somePromise).catch(e => console.error(e));</code></pre>
*
* @param {...function|Iterable.<*>} fns Are functions to compose chains of promises.
* @returns {(promise : Promise|any) => Promise} A function which expects any value as input (resolving to Promise) and returns a Promise.
*/
var acompose = (...fns) => async promise =>
extractResolvedArguments(fns).reduceRight((promise, fn) => {
const promiseHandler = supportsCustomPromiseHandling(fn);
if (promiseHandler) {
return promiseHandler(promise);
}
return promise.then(fn);
}, Promise.resolve(promise));
/* eslint-disable unicorn/prefer-spread */
/**
* Returns a promise which resolved to array of values from a passed iterable.
* An iterable can be a promise to resolve to iterable and then to array.
*
* @param {Promise|Iterable.<*>} arrayLike Is iterable or promise to resolve to.
* @returns {Promise} A promise to resolve to resulting array.
*/
const extractArrayFromArgument = async arrayLike => Array.from(await Promise.resolve(arrayLike));
const filterMergeMap = filterFn => async (element, index, array) => {
const filterResult = !!(await filterFn(element, index, array));
return { filterResult, element };
};
const filterResultsReducer = (filteredArray, { filterResult, element }) => {
if (filterResult) {
filteredArray.push(element);
}
return filteredArray;
};
const getFilteredInParallel = async (filterFn, array) => {
const filterMergeMapFn = filterMergeMap(filterFn);
const filterValues = await Promise.all(array.map(filterMergeMapFn));
return filterValues.reduce(filterResultsReducer, []);
};
const getFilteredInSequence = async (filterFn, array) => {
const result = [];
for (let i = 0; i < array.length; i++) {
const filterResult = !!(await filterFn(array[i], i, array));
if (filterResult) {
result.push(array[i]);
}
}
return result;
};
var afilterGeneric = (sequence = false) => filterFn => async iterable => {
const sourceArray = await extractArrayFromArgument(iterable);
const array = await Promise.all(sourceArray);
return sequence ? await getFilteredInSequence(filterFn, array) : await getFilteredInParallel(filterFn, array);
};
/**
* An asynchronous version of filter over an iterable (afilter stays for async-filter).
*
* It gets an iterable of values (or promises) as input (or promise to resolve to iterable),
* resolves them, iterates over them with filter function
* (which returns boolean where true means current value will be included in resulting array)
* and returns a promise which resolves to an array of values (filtered input iterable).
*
* It allows asynchronous filtering point-free way and can be used with asynchronous compose functions.
*
* It uses Promise.all() under the hood.
* So if filtering function is asynchronous (returns a promise) all promises are being generated at once
* and then resolved with Promise.all().
* So if any of promises will produce error (promise rejection) all the other promises will be invoked anyway.
* The advantage of this method of invoking promises it will finish earlier than sequential filter (because of Promise.all())
* but it may perform some fetches or even state modifications even in case of fail on some previous filtering steps.
*
* <pre><code>const [ first, third ] = await afilter(fetchPermissions)([somePromise1, someValue2, somePromise3]);</code></pre>
*
* It first resolves a promises passed and then pass resolutions value to the filtering function.
*
* Input iterable's values are not restricted to promises but can be any value to pass as input to functions.
*
* It also allows to handle errors like for traditional Promise:
*
* <pre><code>afilter(fetchPermissions)(somePromise1, someValue2, somePromise3).catch(e => console.error(e));</code></pre>
*
* @param {function} filterFn Is filtering function which can produce a promise (but not restricted to this).
* Function can return a promise (asynchronous filtering) or may just perform some synchronous filtering.
* So you can use it in synchronous code taking in mind it returns promise so can't be resolved immediately.
* It has three parameters (currentValue, currentIndex, array) which are already resolved (not promises).
* @returns {(iterable : Promise|Iterable.<*>) => Promise} A function which expects any values as input (resolving to Promise)
* and returns a Promise.
*/
var afilter = afilterGeneric();
/**
* An asynchronous version of filter over an iterable (afilterSeq stays for async-filter).
*
* It gets an iterable of values (or promises) as input (or promise to resolve to iterable),
* resolves them, iterates over them with filter function
* (which returns boolean where true means current value will be included in resulting array)
* and returns a promise which resolves to an array of values (filtered input iterable).
*
* It allows asynchronous filtering point-free way and can be used with asynchronous compose functions.
*
* The difference from regular afilter is if filter function is asynchronous (returns a promise)
* every new invocation of filter function performs sequentially after resolving previous promise.
* So if any of promises produces error (promise rejection) afilterSeq will not produce new promises and they won't be invoked.
*
* <pre><code>const [ first, third ] = await afilterSeq(fetchPermissions)([somePromise1, someValue2, somePromise3]);</code></pre>
*
* It first resolves a promises passed and then pass resolutions value to the filtering function.
*
* Input iterable's values are not restricted to promises but can be any value to pass as input to functions.
*
* It also allows to handle errors like for traditional Promise:
*
* <pre><code>afilterSeq(fetchPermissions)(somePromise1, someValue2, somePromise3).catch(e => console.error(e));</code></pre>
*
* @param {function} filterFn Is filtering function which can produce a promise (but not restricted to this).
* Function can return a promise (asynchronous filtering) or may just perform some synchronous filtering.
* So you can use it in synchronous code taking in mind it returns promise so can't be resolved immediately.
* It has three parameters (currentValue, currentIndex, array) which are already resolved (not promises).
* @returns {(iterable : Promise|Iterable.<*>) => Promise} A function which expects any values as input (resolving to Promise)
* and returns a Promise.
*/
var afilterSeq = afilterGeneric(true);
const getMappedInParallel = (mapFn, array) => Promise.all(array.map(mapFn));
const getMappedInSequence = async (mapFn, array) => {
const result = [];
for (let i = 0; i < array.length; i++) {
const mapResult = await mapFn(array[i], i, array);
result.push(mapResult);
}
return result;
};
var amapGeneric = (sequence = false) => mapFn => async iterable => {
const sourceArray = await extractArrayFromArgument(iterable);
const array = await Promise.all(sourceArray);
return sequence ? getMappedInSequence(mapFn, array) : getMappedInParallel(mapFn, array);
};
/**
* An asynchronous version of map over an iterable (amap stays for async-map).
*
* It gets an iterable of values (or promises) as input (or promise to resolve to iterable),
* resolves them, maps over map function and returns a promise which resolves to an array of values.
*
* It allows asynchronous mapping point-free way and can be used with asynchronous compose functions.
*
* It uses Promise.all() under the hood.
* So if mapping function is asynchronous (returns a promise) all promises are being generated at once
* and then resolved with Promise.all().
* So if any of promises will produce error (promise rejection) all the other promises will be invoked.
* The advantage of this method of invoking promises it will finish earlier than sequential map (because of Promise.all())
* but it may perform some fetches or even state modifications even in case of fail on some previous mapping steps.
*
* <pre><code>const [ first, second, third ] = await amap(getDataFromServer)([somePromise1, someValue2, somePromise3]);</code></pre>
*
* It first resolves a promises passed and then pass resolutions value to the mapping function.
*
* Input iterable's values are not restricted to promises but can be any value to pass as input to functions.
*
* It also allows to handle errors like for traditional Promise:
*
* <pre><code>amap(getDataFromServer)(somePromise1, someValue2, somePromise3).catch(e => console.error(e));</code></pre>
*
* @param {function} mapFn Is mapping function which can produce a promise (but not restricted to this).
* Function can return a promise (asynchronous mapping) or may just perform some synchronous mapping.
* So you can use it in synchronous code taking in mind it returns promise so can't be resolved immediately.
* It has three parameters (currentValue, currentIndex, array) which are resolved (not promises).
* @returns {(iterable : Promise|Iterable.<*>) => Promise} A function which expects any values as input (resolving to Promise)
* and returns a Promise.
*/
var amap = amapGeneric();
/**
* An asynchronous version of map over an iterable (amap stays for async-map).
*
* It gets an iterable of values (or promises) as input (or promise to resolve to iterable),
* resolves them, maps over map function and returns a promise which resolves to an array of values.
*
* It allows asynchronous mapping point-free way and can be used with asynchronous compose functions.
*
* The difference from regular amap is if map function is asynchronous (returns a promise)
* every new invocation of map function performs sequentially after resolving previous promise.
* So if any of promises produces error (promise rejection) amapSeq will not produce new promises and they won't be invoked.
*
* <pre><code>const [ first, second, third ] = await amap(getDataFromServer)([somePromise1, someValue2, somePromise3]);</code></pre>
*
* It first resolves a promises passed and then pass resolutions value to the mapping function.
*
* Input iterable's values are not restricted to promises but can be any value to pass as input to functions.
*
* It also allows to handle errors like for traditional Promise:
*
* <pre><code>amap(getDataFromServer)(somePromise1, someValue2, somePromise3).catch(e => console.error(e));</code></pre>
*
* @param {function} mapFn Is mapping function which can produce a promise (but not restricted to this).
* Function can return a promise (asynchronous mapping) or may just perform some synchronous mapping.
* So you can use it in synchronous code taking in mind it returns promise so can't be resolved immediately.
* It has three parameters (currentValue, currentIndex, array) which are resolved (not promises).
* @returns {(iterable : Promise|Iterable.<*>) => Promise} A function which expects any values as input (resolving to Promise)
* and returns a Promise.
*/
var amapSeq = amapGeneric(true);
/**
* Asynchronous pipe function (apipe stays for async-pipe).
*
* The main purpose is to replace a Promise handling code like this:
* <pre><code>somePromise.then(normalize).then(upperCase).then(insertGreetings);</code></pre>
*
* with point-free style of functional pipe syntax like the following:
* <pre><code>apipe(normalize, upperCase, insertGreetings)(somePromise);</code></pre>
*
* It is lazy and allows of reusing of promise handling chains.
*
* You can run apipe with Promise instance (for true asynchronous execution)
* or with any other object to use as in usual functional composition.
* It produces a Promise and can be used in async/await context:
*
* <pre><code>const message = await apipe(normalize, upperCase, insertGreetings)(somePromise);</code></pre>
*
* It also allows to handle errors like for traditional Promise but only in the tail position of the chain:
*
* <pre><code>apipe(normalize, upperCase, insertGreetings)(somePromise).catch(e => console.error(e));</code></pre>
*
* @param {...function|Iterable.<*>} fns Are functions to pipe chains of promises.
* @returns {(promise : Promise|any) => Promise} A function which expects any value as input (resolving to Promise) and returns a Promise.
*/
var apipe = (...fns) => async promise =>
extractResolvedArguments(fns).reduce((promise, fn) => {
const promiseHandler = supportsCustomPromiseHandling(fn);
if (promiseHandler) {
return promiseHandler(promise);
}
return promise.then(fn);
}, Promise.resolve(promise));
/**
* A kind of composable version of Promise.all().
*
* It gets some value or promise as input, pass it to the functions list
* and produces the array of results after resolving all the functions which can return promises as well.
*
* It allows to use Promise.all() point-free way.
*
* <pre><code>const [ first, second ] = await applyFns(squareRoot, getDataFromServer)(somePromise);</code></pre>
*
* It first resolves a promise passed and then pass resolution value to all the functions.
*
* Input value is not restricted to promise but can be any value to pass as input to functions.
*
* It also allows to handle errors like for traditional Promise:
*
* <pre><code>applyFns(squareRoot, getDataFromServer)(somePromise).catch(e => console.error(e));</code></pre>
*
* @param {...function|Iterable.<*>} fns Are functions to handle input value in parallel.
* Functions can return promises or may just perform some mapping.
* So you can use it in synchronous code taking in mind it returns promise so can't be resolved immediately.
* @returns {(value : Promise|any) => Promise} A function which expects any value as input (resolving to Promise) and returns a Promise.
*/
var applyFns = (...fns) => async value => {
const resolvedValue = await Promise.resolve(value);
return Promise.all(extractResolvedArguments(fns).map(fn => fn(resolvedValue)));
};
/**
* Composable version of promise.then(mapFn).catch(catchFn).
*
* It gets a value (a promise or not), resolves it and handles as promise.then(mapFn).catch(catchFn) returning resulting promise.
*
* It allows to handle errors within acompose or apipe asynchronous composition chains to restore broken state etc.
*
* A sample with acompose:
*
* <pre><code>const resultOrFallback = await acompose(applySafe(canFailFn, handleAndRecoverFn), canFailTooFn)(someInput);</code></pre>
*
* Standalone usage:
*
* <pre><code>const resultOrFallback = await applySafe(canFailFn, handleAndRecoverFn)(requestDataAndReturnPromise());</code></pre>
*
* Or even:
*
* <pre><code>const resultOrFallback = await applySafe(acompose(handlerFn2, handlerFn1), handleAndRecoverFn)(requestDataAndReturnPromise());</code></pre>
*
* It is the same as the following:
*
* <pre><code>requestDataAndReturnPromise().then(canFailFn).catch(handleAndRecoverFn).then(resultOrFallback => console.log(resultOrFallback));</code></pre>
*
* @param {function} mapFn Is function to handle Promise's resolution (then).
* @param {function} catchFn Is function to handle Promise's rejection (catch).
* @returns {any => Promise} A function which expects any value as input (Promise or not) and returns a Promise.
*/
var applySafe = (mapFn, catchFn) => {
const handler = value =>
Promise.resolve(value)
.then(mapFn)
.catch(catchFn);
return addCustomPromiseHandlingSupport(handler, promise => promise.then(mapFn).catch(catchFn));
};
const resolveArrayFromInput = async iterable => Promise.all(await extractArrayFromArgument(iterable));
const reducer = reduceFn => (acc, current, index, array) =>
Promise.resolve(acc).then(acc => reduceFn(acc, current, index, array));
const getReducerArguments = args => {
const effectiveReduceFn = reducer(args[0]);
const effectiveArguments = [...args];
effectiveArguments[0] = effectiveReduceFn;
return effectiveArguments;
};
/**
* Asynchronous composable version of reduce method for iterables ("a" stays for "asynchronous").
*
* It gets a list of values (or list of promises, or promise to resolve to list) and performs standard reduce on them.
*
* Reduce function may be asynchronous to return a promise (to fetch some data etc).
*
* Initial value of reducer also could be a promise.
*
* A sample usage is:
*
* <pre><code>const sum = async (currentSum, invoiceId) => {
* const { total:invoiceTotal } = await fetchInvoiceById(invoiceId);
* return currentSum + invoiceTotal;
* };
* const paymentTotal = await areduce(sum, 0)(fetchInvoiceIds(userId));</code></pre>
*
* Or the same with acompose:
*
* <pre><code>const paymentTotal = await acompose(areduce(sum, 0), fetchInvoiceIds)(userId);</code></pre>
*
* @param {function} callback Function to execute on each element in the array, taking four arguments
* (accumulator, currentValue, currentIndex, array).
* @param {any} initialValue (optional) Value to use as the first argument to the first call of the callback.
* @returns {(iterable : Promise|Iterable.<*>) => Promise} A function which expects an iterable
* (or promise resolved to iterable) and returns a Promise.
*/
var areduce = (...args) => async iterable =>
Array.prototype.reduce.apply(await resolveArrayFromInput(iterable), getReducerArguments(args));
/**
* Asynchronous composable version of reduceRight method for iterables ("a" stays for "asynchronous").
*
* It gets a list of values (or list of promises, or promise to resolve to list) and performs standard reduce on them.
*
* Reduce function may be asynchronous to return a promise (to fetch some data etc).
*
* Initial value of reducer also could be a promise.
*
* A sample usage is:
*
* <pre><code>const sum = async (currentSum, invoiceId) => {
* const { total:invoiceTotal } = await fetchInvoiceById(invoiceId);
* return currentSum + invoiceTotal;
* };
* const paymentTotal = await areduceRight(sum, 0)(fetchInvoiceIds(userId));</code></pre>
*
* Or the same with acompose:
*
* <pre><code>const paymentTotal = await acompose(areduceRight(sum, 0), fetchInvoiceIds)(userId);</code></pre>
*
* @param {function} callback Function to execute on each element in the array, taking four arguments
* (accumulator, currentValue, currentIndex, array).
* @param {any} initialValue (optional) Value to use as the first argument to the first call of the callback.
* @returns {(iterable : Promise|Iterable.<*>) => Promise} A function which expects an iterable
* (or promise resolved to iterable) and returns a Promise.
*/
var areduceRight = (...args) => async iterable =>
Array.prototype.reduceRight.apply(await resolveArrayFromInput(iterable), getReducerArguments(args));
export { acatch, acompose, afilter, afilterSeq, amap, amapSeq, apipe, applyFns, applySafe, areduce, areduceRight };
//# sourceMappingURL=index.js.map