curry a function fn, with optional thisArg option for binding as the this-object.

what is currying? it allows a multi-parameter function to be transformed into a higher order function that always takes one argument, and spits out another function which also take only one argument, and so on... until all parameters have been filled, upon which the final function finally evaluates into the return value (type parameter R in this case).

note that this function relies on fn.length property to identify the number of required arguments taken by fn. this means that default valued arguments (such as c in fn: (a: number, b: number, c = 5) => number), or rest/spread arguments (such as args in fn: (a: number, b: number, ...args: number[]) => number), are not considered as required, and thus do not increment the count of fn.length.

currying is usually poorly implemented through the use of closure. for instance:

const curried_fn = ((arg0) => (arg1) => (arg2) => fn(arg1, arg2, arg3))
const output = curried_fn("arg0")("arg1")("arg2")

this is bad because when you evaluate a curry with N-parameters, you also have to make N-calls (albeit it being tail-calls), instead of just one call, should you have had all the parameters from the beginning. not to mention that all javascript engines famously do not perform tail-call optimizations.

so here, I've implemented currying using the bind method, which means that once all parameters are filled, the function goes through only one call (no overheads). the same example from before would translate into the following when binding is used:

const thisArg = undefined
const curried_fn = fn.bind(thisArg, arg0).bind(thisArg, arg1).bind(thisArg, arg2)
const output = curried_fn("arg0")("arg1")("arg2")

do note that it is very possible for the javascript engine to internally create tail-recursion closure for every argument binding, resulting in the same unoptimized function that we discredited just a while ago. without benchmarking, I cannot say for sure which implementation is more performant.

import { assertEquals as assertEq } from "jsr:@std/assert"

const abcd = (a: number, b: string, c: boolean, d: symbol): string => (String(a) + b + String(c) + " " + String(d))
const abcd_curry = curry(abcd)

abcd_curry satisfies ((arg: number) => (arg: string) => (arg: boolean) => (arg: symbol) => string)

assertEq(
((((abcd_curry(42) satisfies ((arg: string) => (arg: boolean) => (arg: symbol) => string)
)(" hello to za warudo! ") satisfies ((arg: boolean) => (arg: symbol) => string)
)(true) satisfies ((arg: symbol) => string)
)(Symbol.iterator) satisfies (string)
),
"42 hello to za warudo! true Symbol(Symbol.iterator)",
)
  • Type Parameters

    • FN extends (...args: any) => any
    • R extends any = ReturnType<FN>
    • ARGS extends any[] = Parameters<FN>

    Parameters

    • fn: FN

      the function to curry

    • OptionalthisArg: ThisParameterType<FN>

      provide an optional argument to use as the this object inside of fn

    Returns CurrySignature<FN, R, ARGS>

    a series of single argument partial functions that does not evaluate until all parameters have been provided