Skip to content

Commit

Permalink
✨ Add curry function
Browse files Browse the repository at this point in the history
  • Loading branch information
TomokiMiyauci committed Jun 14, 2021
1 parent a581145 commit 45ec551
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 0 deletions.
129 changes: 129 additions & 0 deletions curry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright 2021-present the Curry authors. All rights reserved. MIT license.

// deno-lint-ignore-file no-explicit-any

/**
* Alias for Any array types
*
* @internal
*/
type AnyArray = readonly unknown[];

/**
* @internal
*/
type Union2Intersection<U> = (
U extends unknown ? (arg: U) => void : never
) extends (arg: infer I) => void ? I
: never;

/**
* Pop types
* @typeParams - T Any array
* @example
* ```ts
* Pop<[1, 2, 3]> = [1, 2].
* ```
*
* @internal
*/
type Pop<T extends AnyArray> = T extends [...infer Head, unknown] ? Head
: T extends readonly [...infer Head, unknown] ? readonly [...Head]
: never;

/**
* Shift types
* @typeParams S - Any array
* @example
* ```ts
* Shift<[1], [1, 2, 3]> = [2, 3].
* Shift<[1, 2], [1, 2, 3]> = [3].
* ```
*
* @internal
*/
type Shift<S extends AnyArray, T extends AnyArray> = T extends [
...S,
...infer Rest,
] ? Rest
: never;

/**
* @example
* ```ts
* UnionFactorial<[1, 2, 3]> = [1] | [1, 2] | [1, 2, 3].
* ```
*
* @internal
*/
type UnionFactorial<T extends AnyArray> = T extends readonly [] ? never
: T | UnionFactorial<Pop<T>>;

/**
* @example
* ```ts
* OverloadsByArgs<[1] | [1, 2], [1, 2, 3], 7> =
* | CurriedWithFixArgs<[1], [2, 3], 7>
* | CurriedWithFixArgs<[1, 2], [3], 7>.
* ```
*
* @internal
*/
type OverloadsByArgs<
Args extends AnyArray,
FullArgs extends AnyArray,
ReturnValue,
> = Args extends unknown
? CurriedWithFixArgs<Args, Shift<Args, FullArgs>, ReturnValue>
: never;

/**
* @internal
*/
type CurriedWithFixArgs<
Args extends AnyArray,
RestArgs extends AnyArray,
ReturnValue,
> = (...args: Args) => Curried<RestArgs, ReturnValue>;

/**
* Curry types
*
* @internal
*/
type Curried<Args extends AnyArray, ReturnValue> = Args extends [] ? ReturnValue
: Union2Intersection<
OverloadsByArgs<UnionFactorial<Args>, Args, ReturnValue>
>;

/**
* Creates a function that accepts arguments of `fn` and either invokes `fn` returning its result, if at least arity number of arguments have been provided, or returns a function that accepts the remaining `fn` arguments, and so on.
*
* @param fn - The function to curry
* @returns The new curried function
*
* @remarks
* Maximum number of arity is `16`. Beyond that, the type system will breaks.
*
* @example
* ```ts
* const replace = (from: string, to: string, val: string) => val.replace(from, to)
* const curriedReplace = curry(replace)
* const curriedReplace('hello', 'hi', 'hello world') // 'hi world'
* const curriedReplace('hello')('hi', 'hello world') // 'hi world'
* const curriedReplace('hello')('hi')('hello world') // 'hi world'
* ```
*
* @beta
*/
const curry = <T extends unknown[], R>(
fn: (...args: T) => R,
): T["length"] extends 0 ? () => R : Curried<T, R> => {
const curried: any = (...t: T) =>
t.length >= fn.length ? fn(...t) : curried.bind(null, ...t);

return curried;
};

export { curry };
export type { Pop, Shift, UnionFactorial };
128 changes: 128 additions & 0 deletions curry_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2021-present the Curry authors. All rights reserved. MIT license.
import { Pop, Shift, UnionFactorial } from "./curry.ts";
import { curry } from "./curry.ts";
import { assertEquals, assertEqualsTypes } from "./dev_deps.ts";

const arity0 = () => true;
const arity1 = (a: unknown) => a;
const arity2 = (a: unknown, b: unknown) => [a, b];
const arity3 = (a: unknown, b: unknown, c: unknown) => [a, b, c];
const arity4 = (a: unknown, b: unknown, c: unknown, d: unknown) => [a, b, c, d];
const arity5 = (a: unknown, b: unknown, c: unknown, d: unknown, e: unknown) => [
a,
b,
c,
d,
e,
];
const arityMax = (
a: unknown,
b: unknown,
c: unknown,
d: unknown,
e: unknown,
f: unknown,
g: unknown,
h: unknown,
i: unknown,
j: unknown,
k: unknown,
l: unknown,
m: unknown,
n: unknown,
o: unknown,
p: unknown,
q: unknown,
r: unknown,
s: unknown,
t: unknown,
) => [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t];

Deno.test("curry", () => {
const add = curry((a: number, b: number) => a + b);

assertEquals(add(1, 2), 3);
assertEquals(add(1)(2), 3);

assertEquals(curry(arity0)(), true);
assertEquals(curry(arity1)(""), "");
assertEquals(typeof curry(arity2)(""), "function");
assertEquals(curry(arity2)("", 1), ["", 1]);
assertEquals(curry(arity2)("")(1), ["", 1]);

assertEquals(typeof curry(arity3)(""), "function");
assertEquals(typeof curry(arity3)("")(1), "function");
assertEquals(curry(arity3)("")(1)(false), ["", 1, false]);
assertEquals(curry(arity3)("", 1)(false), ["", 1, false]);
assertEquals(curry(arity3)("", 1, false), ["", 1, false]);

assertEquals(curry(arity4)("")(1)(false)(true), ["", 1, false, true]);
assertEquals(curry(arity4)("", 1)(false)(true), ["", 1, false, true]);
assertEquals(curry(arity4)("", 1, false)(true), ["", 1, false, true]);
assertEquals(curry(arity4)("", 1, false, true), ["", 1, false, true]);

assertEquals(curry(arity5)("")(1)(false)(true)(0n), ["", 1, false, true, 0n]);
assertEquals(curry(arity5)("", 1)(false)(true)(0n), ["", 1, false, true, 0n]);
assertEquals(curry(arity5)("", 1, false)(true)(0n), ["", 1, false, true, 0n]);
assertEquals(curry(arity5)("", 1, false, true)(0n), ["", 1, false, true, 0n]);
assertEquals(curry(arity5)("", 1, false, true, 0n), ["", 1, false, true, 0n]);
assertEquals(curry(arity5)("")(1, false, true, 0n), ["", 1, false, true, 0n]);
assertEquals(curry(arity5)("")(1)(false, true, 0n), ["", 1, false, true, 0n]);
assertEquals(curry(arity5)("")(1)(false)(true, 0n), ["", 1, false, true, 0n]);

assertEqualsTypes<Pop<[]>, never>();
assertEqualsTypes<Pop<readonly []>, never>();
assertEqualsTypes<Pop<[0]>, []>();
assertEqualsTypes<Pop<readonly [0]>, readonly []>();
assertEqualsTypes<Pop<[0, 0]>, [0]>();
assertEqualsTypes<Pop<readonly [0, 0]>, readonly [0]>();
assertEqualsTypes<Pop<[[]]>, []>();

assertEqualsTypes<Shift<[], []>, []>();
assertEqualsTypes<Shift<[1], []>, never>();
assertEqualsTypes<Shift<[1], [1]>, []>();
assertEqualsTypes<Shift<[1, 2], [1]>, never>();

assertEqualsTypes<UnionFactorial<[]>, never>();
assertEqualsTypes<UnionFactorial<readonly []>, never>();
assertEqualsTypes<UnionFactorial<[1]>, [1]>();
assertEqualsTypes<UnionFactorial<readonly [1]>, readonly [1]>();
assertEqualsTypes<UnionFactorial<[1, 2]>, [1] | [1, 2]>();
assertEqualsTypes<
UnionFactorial<readonly [1, 2]>,
readonly [1] | readonly [1, 2]
>();
assertEqualsTypes<UnionFactorial<[1, 2, 3]>, [1] | [1, 2] | [1, 2, 3]>();

assertEqualsTypes<() => boolean>(curry(arity0));
assertEqualsTypes<unknown>(curry(arity1)(""));
assertEqualsTypes<unknown[]>(curry(arity2)("")(""));
assertEqualsTypes<unknown[]>(curry(arity2)("", ""));
assertEqualsTypes<unknown[]>(curry(arity3)("", "", ""));
assertEqualsTypes<unknown[]>(curry(arity3)("", "", ""));
assertEqualsTypes<unknown[]>(curry(arity3)("")("")(""));
assertEqualsTypes<unknown[]>(
curry(arityMax)(
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
),
);
});
3 changes: 3 additions & 0 deletions dev_deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Copyright 2021-present the Curry authors. All rights reserved. MIT license.
export { assertEquals } from "https://deno.land/std@0.97.0/testing/asserts.ts";
export const assertEqualsTypes = <T, U extends T = T>(_actual?: U): void => {};
2 changes: 2 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Copyright 2021-present the Curry authors. All rights reserved. MIT license.
export { curry } from "./curry.ts";

0 comments on commit 45ec551

Please sign in to comment.