func/tools.js

/**
 * Functional programming tools.
 *
 * @module func
 * @license Apache-2.0
 * @copyright Mat. 2018-present
 */

import { findDuplicates } from "../array/set";
import { head, last } from "../array/list";
import { range } from "../array/tools";
import { curryN } from "../func/curry";
import { isArray, isFunction } from "../type/check";




/**
 * Handle exceptions in expressions.
 *
 * @function handleException
 * @param {Function} fn
 * @param {Function} [handler]
 * @returns {any}
 */
export const handleException = (fn, handler = identity) => {
    try { return fn(); }
    catch (ex) { return isFunction(handler) ? handler(ex) : ex; }
};




/**
 * Return value passed as a first argument.
 *
 * @function identity
 * @param {any} x
 * @returns {any}
 */
export const identity = x => x;




/**
 * Put a given argument under function abstraction.
 *
 * @function lazyish
 * @param {any} x
 * @returns {Function}
 */
export const lazyish = x => () => x;




/**
 * Local binding.
 *
 * Inspired by {@link https://github.com/kongware/scriptum}
 *
 * @function local
 * @param {Function} () => T
 * @returns {*} T
 */
export const local = (f = identity) => f();




/**
 * Create function that can "lock the thing".
 *
 * During the first `n` invocations returned function acts as identity.
 * During the `n+1` invocation the argument `thing` is memoized
 * and on all subsequent invocations passed arguments are ignored
 * and memoized `thing` is returned.
 *
 * ```
 * let lock = func.locker()
 *
 * lock("I like you!")
 * 'I like you!'
 *
 * lock("I hate you.")
 * 'I like you!'
 *
 * let lock2 = func.locker(2)
 *
 * lock2("Repeat after me!")
 * 'Repeat after me!'
 *
 * lock2(42)
 * 42
 *
 * lock2("All right...")
 * 42
 * ```
 *
 * @function locker
 * @param {Number} [n=1]
 * @returns {Function} (any) => any
 */
export const locker = (n = 1) => (
    ({ memoized, value }) =>
        (thing) => {
            if (memoized < n) {
                memoized += 1;
                value = thing;
            }
            return value;
        }
) ({ memoized: 0, value: null });




/**
 * Function arguments rearrangement.
 *
 * Takes function `f` and `indices` and returns a new function,
 * which has it's arguments arranged according to `indices`.
 *
 * Returned function will expect the number of arguments to be
 * no less than the number of `indices`. If not all of the required
 * arguments will be passed, a new function will be returned
 * expecting _rest_ of the arguments.
 *
 * In other words - function returned by `rearg` is *curried*.
 *
 * Example:
 *
 * ```
 * string.padLeft("Foo", 10, ".")  ->  ".......Foo"
 *
 * let rePad = func.rearg(string.padLeft) (1, 2, 0)  // *curried* form
 * rePad(10, ".", "Bar")  ->  ".......Bar"
 *
 * console.log("a", "b", "c", "d", "e")
 * a b c d e
 *
 * let revConsole = func.rearg(console.log) (4, 3, 2, 1, 0)
 * revConsole("a", "b", "c", "d", "e")
 * e d c b a
 *
 * revConsole("f") ("g", "h") ("i") ("j")
 * j i h g f
 * ```
 *
 * @function rearg
 * @param {Function} f
 * @returns {Function}
 */
export const rearg = f => (...indices) => {
    if (indices.length === 0) return f;

    if (findDuplicates(indices).length > 0) throw RangeError(
        "func.rearg: duplicate indices are forbidden",
    );

    // index mapping "new" -> "old"
    const indexPairs = indices
        .map((n, o) => [n, o])
        .sort(([n1], [n2]) => n1 - n2);

    return curryN(
        indices.length,
        (...args) => {
            const
                // source arguments: [argument, usageCount]
                sargs = args.map(a => [a, 0]),

                // destination arguments: [argument, usageCount]
                dargs = range(Math.max(
                    head(last(indexPairs)) + 1,
                    args.length,
                )).map(() => [null, 0]);

            // not used source arguments
            let rest = null;

            // fill destination arguments with source arguments
            // (through index mapping) and mark valid destination arguments
            // and used source arguments
            indexPairs.forEach(([n, o]) => {
                dargs[n][0] = head(sargs[o]);
                dargs[n][1] += 1;
                sargs[o][1] += 1;
            });

            // filter-out all used source arguments and leave only unused ones
            rest = sargs.filter((a) => last(a) === 0).reverse();

            // return function `f` invocation with valid destination arguments
            // and not-valid ones replaced with the unused source arguments
            return f(...dargs.map(a => {
                if (last(a) !== 0) return head(a);
                const rel = rest.pop();
                if (isArray(rel)) return head(rel);
                return rel;
            }));
        },
    );
};