func/curry.ts

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

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */

import type { Fun } from "../type/defs";
import type { Head, Length, Tail } from "../type/list";




/**
 * Recursive type for curried functions.
 */
export type CurryFun<
    F extends Fun,
    P extends any[] = Parameters<F>,
    R = ReturnType<F>,
> = Length<P> extends 0
        ? () => R
        : Length<P> extends 1
            ? (x: Head<P>) => R
            : (x: Head<P>) => CurryFun<(...args: Tail<P>) => R>;




/**
 * `curry` and `curryN` return type.
 */
type CurryReturnType<
    F extends Fun,
> = Length<Parameters<F>> extends 0 | 1
    ? F
    : F & CurryFun<F>;




/**
 * Recursive type for thunk-curried functions.
 */
export type ThunkFun<
    F extends Fun,
    P extends any[] = Parameters<F>,
    R = ReturnType<F>,
> = Length<P> extends 0
    ? () => R
    : (x: Head<P>) => ThunkFun<(...args: Tail<P>) => R>;




/**
 * Return curried form of a given function `f`.
 *
 * If funcion `f` has _arity_ 3, and `g = curry(f)` then
 * a following invocations have the same result:
 *
 * ```
 * g(a,  b,  c)
 * g(a,  b) (c)
 * g(a) (b,  c)
 * g(a) (b) (c)
 * ```
 *
 * Function `f`'s _arity_ is obtained by checking it's `.length`
 * property, so if function `f` is defined with a _rest parameter_
 * then this parameter is excluded. Also, only parameters before
 * the first one with a default value are included.
 *
 * @function curry
 * @param {Function} f
 * @returns {CurryFun}
 */
export function curry<F extends Fun> (f: F): CurryReturnType<F> {
    return curryN(f.length, f);
}




/**
 * Translate the evaluation of function `f` taking `n` arguments
 * into an evaluation of sequence of `n` functions, where each
 * next function is a result of previous function evaluation.
 *
 * ```
 * f(a, b, c, d, e)  <=>  curryN(5, f) (a) (b) (c) (d) (e)
 * ```
 *
 * @function curryN
 * @param {Number} n
 * @param {Function} f
 * @returns {CurryFun}
 */
export function curryN<F extends Fun> (n: number, f: F): CurryReturnType<F> {
    return (
        (...args: any[]) =>
            n <= args.length
                ? f(...args)
                : curryN(n - args.length, partial(f) (...args))
    ) as CurryReturnType<F>;
}




/**
 * Translate the evaluation of function `f` taking multiple arguments
 * into an evaluation of sequence of functions, each with a single argument.
 *
 * Because `curryThunk` doesn't assume anything on passed function
 * `f` _arity_, final invocation has to be done with no arguments.
 *
 * ```
 * f(a, b, c, d)  <=>  curryThunk(f) (a) (b) (c) (d) ()
 * ```
 *
 * @function curryThunk
 * @param {Function} f
 * @returns {ThunkFun}
 */
export function curryThunk<F extends Fun> (f: F): ThunkFun<F> {
    return (
        (...args: any[]) =>
            args.length === 0
                ? f()
                : curryThunk(partial(f) (...args))
    ) as ThunkFun<F>;
}




/**
 * Partial application.
 *
 * Bind `init` arguments to function `f` and construct
 * a function of smaller _arity_ which accept `rest` of the arguments.
 *
 * Example:
 *
 * ```
 * let f = (a, b) => a + b
 * f(3, 4)  ->  7
 * let g = partial(f) (3)  // note that `partial` is in *curried* form
 * g(4)  ->  7
 * ```
 *
 * @function partial
 * @param {Function} f
 * @returns {Function}
 */
export function partial<F extends Fun> (f: F): (...init: any[]) => Fun {
    return (...init) => (...rest) => f(...[...init, ...rest]);
}