From 936439071529c46b749dc92054bfe2ec6022a930 Mon Sep 17 00:00:00 2001 From: seungrodotlee Date: Sat, 14 Sep 2024 14:24:56 +0900 Subject: [PATCH] feat(curry): add `curry` function (#187) * feat. Add utility types * feat. Add curry function * feat. add error throw * feat. add spec of curry * fix. fix type error when optional parameter exist * fix. fix type when rest parameters exist * fix. fix types error * fix. fix all type errors * refactor. rename OptionalStartIdx * test. add test codes when rest parameters exist * feat. add method to flexibleCurry * feat. fix all type errors and functional errors * chore. add benchmark * chore. fix comments * refactor. enhance type of flexibleCurry * refactor. refactor arrow function -> normal function * feat. correctly infer the errored type when the run method is called while the argument to be received is not optional * docs. add docs about curry * refactor. file refactor naming case of types * fix. fix types error caused by renaming of case * fix. fix errors same as last commit * fix. fix errors same as last commit * feat. keep curry function and type simple * fix. fix test codes with updated implementation * Update src/function/index.ts * feat(curry): Fix curry types * feat(curry): Fix curry types * feat(curry): Fix curry types * Remove ramda --------- Co-authored-by: Sojin Park Co-authored-by: Sojin Park --- benchmarks/curry.bench.ts | 15 ++ docs/ja/reference/function/curry.md | 42 ++++++ docs/ko/reference/function/curry.md | 44 ++++++ docs/reference/function/curry.md | 43 ++++++ docs/zh_hans/reference/function/curry.md | 42 ++++++ package.json | 2 +- src/function/curry.spec.ts | 31 ++++ src/function/curry.ts | 180 +++++++++++++++++++++++ src/function/index.ts | 1 + 9 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 benchmarks/curry.bench.ts create mode 100644 docs/ja/reference/function/curry.md create mode 100644 docs/ko/reference/function/curry.md create mode 100644 docs/reference/function/curry.md create mode 100644 docs/zh_hans/reference/function/curry.md create mode 100644 src/function/curry.spec.ts create mode 100644 src/function/curry.ts diff --git a/benchmarks/curry.bench.ts b/benchmarks/curry.bench.ts new file mode 100644 index 00000000..1c431e50 --- /dev/null +++ b/benchmarks/curry.bench.ts @@ -0,0 +1,15 @@ +import { bench, describe } from 'vitest'; +import { curry as curryToolkit } from 'es-toolkit'; +import { curry as curryLodash } from 'lodash'; + +describe('curry', () => { + const fn = (a: number, b: string, c: boolean) => ({ a, b, c }); + + bench('es-toolkit/curry', () => { + curryToolkit(fn)(1)('a')(true); + }); + + bench('lodash/curry', () => { + curryLodash(fn)(1)('a')(true); + }); +}); diff --git a/docs/ja/reference/function/curry.md b/docs/ja/reference/function/curry.md new file mode 100644 index 00000000..3edd11d1 --- /dev/null +++ b/docs/ja/reference/function/curry.md @@ -0,0 +1,42 @@ +# curry + +関数をカリー化し、1回に1つのパラメータで呼び出すことができ、次のパラメータを取る新しい関数を返し続けます。 +このプロセスはすべてのパラメータが提供されるまで続き、すべてのパラメータが蓄積された段階で元の関数が呼び出されます。 + +## インターフェース +```typescript +function curry(func: () => R): () => R; +function curry(func: (p: P) => R): (p: P) => R; +function curry(func: (p1: P1, p2: P2) => R): (p1: P1) => (p2: P2) => R; +function curry(func: (p1: P1, p2: P2, p3: P3) => R): (p1: P1) => (p2: P2) => (p3: P3) => R; +function curry(func: (p1: P1, p2: P2, p3: P3, p4: P4) => R): (p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => R; +function curry(func: (p1: P1, p2: P2, p3: P3, p4: P4, p5: P5) => R): (p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => (p5: P5) => R; +function curry(func: (...args: any[]) => any): (...args: any[]) => any; +function curry(func: (...args: any[]) => any): (...args: any[]) => any; +``` + +### パラメータ + +- `func` (`(...args: any[]) => any`): カリー化する関数。 + +### 戻り値 + +(`(...args: any[]) => any`): 1回に1つのパラメータで呼び出すことができるカリー化された関数。 + +## 例 +```typescript +function sum(a: number, b: number, c: number) { + return a + b + c; +} + +const curriedSum = curry(sum); + +// パラメータ `a` に値 `10` を与えます。 +const sum10 = curriedSum(10); + +// パラメータ `b` に値 `15` を与えます。 +const sum25 = sum10(15); + +// パラメータ `c` に値 `5` を与えます。 +// 関数 'sum' はすべてのパラメータを受け取ったので、今値を返します。 +const result = sum25(5); diff --git a/docs/ko/reference/function/curry.md b/docs/ko/reference/function/curry.md new file mode 100644 index 00000000..fbd6e399 --- /dev/null +++ b/docs/ko/reference/function/curry.md @@ -0,0 +1,44 @@ +# curry + +함수를 커링하여 한 번에 하나의 파라미터로 호출할 수 있도록 하고, 다음 파라미터를 받는 새로운 함수를 반환해요. +모든 파라미터가 제공되면, 이때 원래 함수가 지금까지 주어진 파라미터로 호출돼요. + +## 인터페이스 + +```typescript +function curry(func: () => R): () => R; +function curry(func: (p: P) => R): (p: P) => R; +function curry(func: (p1: P1, p2: P2) => R): (p1: P1) => (p2: P2) => R; +function curry(func: (p1: P1, p2: P2, p3: P3) => R): (p1: P1) => (p2: P2) => (p3: P3) => R; +function curry(func: (p1: P1, p2: P2, p3: P3, p4: P4) => R): (p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => R; +function curry(func: (p1: P1, p2: P2, p3: P3, p4: P4, p5: P5) => R): (p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => (p5: P5) => R; +function curry(func: (...args: any[]) => any): (...args: any[]) => any; +``` + +### 파라미터 + +- `func` (`(...args: any[]) => any`): 커링할 함수예요. + +### 반환 값 + +(`(...args: any[]) => any`): 한 번에 하나의 파라미터로 호출할 수 있는 커링된 함수예요. + +## 예시 + +```typescript +function sum(a: number, b: number, c: number) { + return a + b + c; +} + +const curriedSum = curry(sum); + +// 파라미터 `a`로 값 `10`을 제공해요. +const sum10 = curriedSum(10); + +// 파라미터 `b`로 값 `15`을 제공해요. +const sum25 = sum10(15); + +// 파라미터 `c`로 값 `5`을 제공해요. +// 함수 'sum'은 모든 파라미터를 받았기 때문에, 이제 값을 반환해요. +const result = sum25(5); +``` \ No newline at end of file diff --git a/docs/reference/function/curry.md b/docs/reference/function/curry.md new file mode 100644 index 00000000..6749ef39 --- /dev/null +++ b/docs/reference/function/curry.md @@ -0,0 +1,43 @@ +# curry + +Curries a function, allowing it to be called with a single argument at a time and returning a new function that takes the next argument. +This process continues until all arguments have been provided, at which point the original function is called with all accumulated arguments. + +## Signature + +```typescript +function curry(func: () => R): () => R; +function curry(func: (p: P) => R): (p: P) => R; +function curry(func: (p1: P1, p2: P2) => R): (p1: P1) => (p2: P2) => R; +function curry(func: (p1: P1, p2: P2, p3: P3) => R): (p1: P1) => (p2: P2) => (p3: P3) => R; +function curry(func: (p1: P1, p2: P2, p3: P3, p4: P4) => R): (p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => R; +function curry(func: (p1: P1, p2: P2, p3: P3, p4: P4, p5: P5) => R): (p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => (p5: P5) => R; +function curry(func: (...args: any[]) => any): (...args: any[]) => any; +``` + +### Parameters + +- `func` (`Function`): The function to curry. + +### Returns + +(`Function`): A curried function that can be called with a single argument at a time. + +## Examples + +```typescript +function sum(a: number, b: number, c: number) { + return a + b + c; +} + +const curriedSum = curry(sum); + +// The parameter `a` should be given the value `10`. +const sum10 = curriedSum(10); + +// The parameter `b` should be given the value `15`. +const sum25 = sum10(15); + +// The parameter `c` should be given the value `5`. The function 'sum' has received all its arguments and will now return a value. +const result = sum25(5); +``` \ No newline at end of file diff --git a/docs/zh_hans/reference/function/curry.md b/docs/zh_hans/reference/function/curry.md new file mode 100644 index 00000000..fbbe4cfb --- /dev/null +++ b/docs/zh_hans/reference/function/curry.md @@ -0,0 +1,42 @@ +# curry + +将一个函数柯里化,允许它每次只用一个参数调用,并返回一个接受下一个参数的新函数。这个过程会继续,直到所有参数都已提供,此时将使用所有累积的参数调用原始函数。 + +## 签名 + +```typescript +function curry(func: () => R): () => R; +function curry(func: (p: P) => R): (p: P) => R; +function curry(func: (p1: P1, p2: P2) => R): (p1: P1) => (p2: P2) => R; +function curry(func: (p1: P1, p2: P2, p3: P3) => R): (p1: P1) => (p2: P2) => (p3: P3) => R; +function curry(func: (p1: P1, p2: P2, p3: P3, p4: P4) => R): (p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => R; +function curry(func: (p1: P1, p2: P2, p3: P3, p4: P4, p5: P5) => R): (p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => (p5: P5) => R; +function curry(func: (...args: any[]) => any): (...args: any[]) => any; +``` + +### 参数 + +- `func` (`(...args: any[]) => any`): 要进行柯里化的函数。 + +### 返回值 + +(`(...args: any[]) => any`): 一个可以每次调用一个参数的柯里化函数。 + +## 示例 + +```typescript +function sum(a: number, b: number, c: number) { + return a + b + c; +} + +const curriedSum = curry(sum); + +// The parameter `a` should be given the value `10`. +const sum10 = curriedSum(10); + +// The parameter `b` should be given the value `15`. +const sum25 = sum10(15); + +// The parameter `c` should be given the value `5`. The function 'sum' has received all its arguments and will now return a value. +const result = sum25(5); +``` \ No newline at end of file diff --git a/package.json b/package.json index 69525934..7b690866 100644 --- a/package.json +++ b/package.json @@ -171,4 +171,4 @@ "format": "prettier --write .", "transform": "jscodeshift -t ./.scripts/tests/transform-lodash-test.ts $0 && prettier --write $0" } -} \ No newline at end of file +} diff --git a/src/function/curry.spec.ts b/src/function/curry.spec.ts new file mode 100644 index 00000000..b44d7650 --- /dev/null +++ b/src/function/curry.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, expectTypeOf, it } from 'vitest'; +import { curry } from './curry'; + +describe('curry', () => { + it('should return original type of function when function without any arguments received', () => { + const fn = () => 'test'; + const curried = curry(fn); + + expect(curried()).toBe('test'); + }); + + it('should curry based on the number of arguments given', () => { + const fn = (a: number, b: number, c: number, d: number) => [a, b, c, d]; + const curried = curry(fn); + const expected = [1, 2, 3, 4]; + + expect(curried(1)(2)(3)(4)).toEqual(expected); + }); + + it('should inference type correctly', () => { + const fn = (a: number, b: string, c: boolean) => ({ a, b, c }); + const curried = curry(fn); + + expectTypeOf(curried).parameters.toEqualTypeOf<[number]>(); + expectTypeOf(curried(1)).parameters.toEqualTypeOf<[string]>(); + expectTypeOf(curried(1)).not.toEqualTypeOf<{ a: number; b: string; c: boolean }>(); + expectTypeOf(curried(1)('a')).parameters.toEqualTypeOf<[boolean]>(); + expectTypeOf(curried(1)('a')).not.toEqualTypeOf<{ a: number; b: string; c: boolean }>(); + expectTypeOf(curried(1)('a')(true)).toEqualTypeOf<{ a: number; b: string; c: boolean }>(); + }); +}); diff --git a/src/function/curry.ts b/src/function/curry.ts new file mode 100644 index 00000000..9d403a8a --- /dev/null +++ b/src/function/curry.ts @@ -0,0 +1,180 @@ +/** + * Curries a function, allowing it to be called with a single argument at a time and returning a new function that takes the next argument. + * This process continues until all arguments have been provided, at which point the original function is called with all accumulated arguments. + * + * @param {() => R} func - The function to curry. + * @returns {() => R} A curried function. + * + * @example + * function noArgFunc() { + * return 42; + * } + * const curriedNoArgFunc = curry(noArgFunc); + * console.log(curriedNoArgFunc()); // 42 + */ +export function curry(func: () => R): () => R; + +/** + * Curries a function, allowing it to be called with a single argument at a time and returning a new function that takes the next argument. + * This process continues until all arguments have been provided, at which point the original function is called with all accumulated arguments. + * + * @param {(p: P) => R} func - The function to curry. + * @returns {(p: P) => R} A curried function. + * + * @example + * function oneArgFunc(a: number) { + * return a * 2; + * } + * const curriedOneArgFunc = curry(oneArgFunc); + * console.log(curriedOneArgFunc(5)); // 10 + */ +export function curry(func: (p: P) => R): (p: P) => R; + +/** + * Curries a function, allowing it to be called with a single argument at a time and returning a new function that takes the next argument. + * This process continues until all arguments have been provided, at which point the original function is called with all accumulated arguments. + * + * @param {(p1: P1, p2: P2) => R} func - The function to curry. + * @returns {(p1: P1) => (p2: P2) => R} A curried function. + * + * @example + * function twoArgFunc(a: number, b: number) { + * return a + b; + * } + * const curriedTwoArgFunc = curry(twoArgFunc); + * const add5 = curriedTwoArgFunc(5); + * console.log(add5(10)); // 15 + */ +export function curry(func: (p1: P1, p2: P2) => R): (p1: P1) => (p2: P2) => R; + +/** + * Curries a function, allowing it to be called with a single argument at a time and returning a new function that takes the next argument. + * This process continues until all arguments have been provided, at which point the original function is called with all accumulated arguments. + * + * @param {(p1: P1, p2: P2, p3: P3, p4: P4) => R} func - The function to curry. + * @returns {(p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => R} A curried function. + * + * @example + * function fourArgFunc(a: number, b: number, c: number, d: number) { + * return a + b + c + d; + * } + * const curriedFourArgFunc = curry(fourArgFunc); + * const add1 = curriedFourArgFunc(1); + * const add2 = add1(2); + * const add3 = add2(3); + * console.log(add3(4)); // 10 + */ +export function curry(func: (p1: P1, p2: P2, p3: P3) => R): (p1: P1) => (p2: P2) => (p3: P3) => R; + +/** + * Curries a function, allowing it to be called with a single argument at a time and returning a new function that takes the next argument. + * This process continues until all arguments have been provided, at which point the original function is called with all accumulated arguments. + * + * @param {(p1: P1, p2: P2, p3: P3, p4: P4) => R} func - The function to curry. + * @returns {(p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => R} A curried function. + * + * @example + * function fourArgFunc(a: number, b: number, c: number, d: number) { + * return a + b + c + d; + * } + * const curriedFourArgFunc = curry(fourArgFunc); + * const add1 = curriedFourArgFunc(1); + * const add2 = add1(2); + * const add3 = add2(3); + * console.log(add3(4)); // 10 + */ +export function curry( + func: (p1: P1, p2: P2, p3: P3, p4: P4) => R +): (p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => R; + +/** + * Curries a function, allowing it to be called with a single argument at a time and returning a new function that takes the next argument. + * This process continues until all arguments have been provided, at which point the original function is called with all accumulated arguments. + * + * @param {(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5) => R} func - The function to curry. + * @returns {(p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => (p5: P5) => R} A curried function. + * + * @example + * function fiveArgFunc(a: number, b: number, c: number, d: number, e: number) { + * return a + b + c + d + e; + * } + * const curriedFiveArgFunc = curry(fiveArgFunc); + * const add1 = curriedFiveArgFunc(1); + * const add2 = add1(2); + * const add3 = add2(3); + * const add4 = add3(4); + * console.log(add4(5)); // 15 + */ +export function curry( + func: (p1: P1, p2: P2, p3: P3, p4: P4, p5: P5) => R +): (p1: P1) => (p2: P2) => (p3: P3) => (p4: P4) => (p5: P5) => R; + +/** + * Curries a function, allowing it to be called with a single argument at a time and returning a new function that takes the next argument. + * This process continues until all arguments have been provided, at which point the original function is called with all accumulated arguments. + * + * @param {(...args: any[]) => any} func - The function to curry. + * @returns {(...args: any[]) => any} A curried function that can be called with a single argument at a time. + * + * @example + * function sum(a: number, b: number, c: number) { + * return a + b + c; + * } + * + * const curriedSum = curry(sum); + * + * // The parameter `a` should be given the value `10`. + * const sum10 = curriedSum(10); + * + * // The parameter `b` should be given the value `15`. + * const sum25 = sum10(15); + * + * // The parameter `c` should be given the value `5`. The function 'sum' has received all its arguments and will now return a value. + * const result = sum25(5); + */ +export function curry(func: (...args: any[]) => any): (...args: any[]) => any; + +/** + * Curries a function, allowing it to be called with a single argument at a time and returning a new function that takes the next argument. + * This process continues until all arguments have been provided, at which point the original function is called with all accumulated arguments. + * + * @param {(...args: any[]) => any} func - The function to curry. + * @returns {(...args: any[]) => any} A curried function that can be called with a single argument at a time. + * + * @example + * function sum(a: number, b: number, c: number) { + * return a + b + c; + * } + * + * const curriedSum = curry(sum); + * + * // The parameter `a` should be given the value `10`. + * const sum10 = curriedSum(10); + * + * // The parameter `b` should be given the value `15`. + * const sum25 = sum10(15); + * + * // The parameter `c` should be given the value `5`. The function 'sum' has received all its arguments and will now return a value. + * const result = sum25(5); + */ +export function curry(func: (...args: any[]) => any): (...args: any[]) => any { + if (func.length === 0 || func.length === 1) { + return func; + } + + return function (arg: any) { + return makeCurry(func, func.length, [arg]); + } as any; +} + +function makeCurry any>(origin: F, argsLength: number, args: any[]) { + if (args.length === argsLength) { + return origin(...args); + } else { + const next = function (arg: Parameters[0]) { + return makeCurry(origin, argsLength, [...args, arg]); + }; + + return next as any; + } +} diff --git a/src/function/index.ts b/src/function/index.ts index a4f3f914..511421ac 100644 --- a/src/function/index.ts +++ b/src/function/index.ts @@ -11,4 +11,5 @@ export { unary } from './unary.ts'; export { partial } from './partial.ts'; export { partialRight } from './partialRight.ts'; export { rest } from './rest.ts'; +export { curry } from './curry.ts'; export { spread } from './spread.ts';