feat(compat): implement curry (#535)

* feat(compat): implement curry

* make lint happy

* doc
This commit is contained in:
D-Sketon 2024-09-15 16:57:27 +08:00 committed by GitHub
parent 9bb1203b51
commit 02ce5e9024
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 436 additions and 15 deletions

View File

@ -1,15 +0,0 @@
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);
});
});

View File

@ -0,0 +1,44 @@
import { bench, describe } from 'vitest';
import { curry as curryToolkit } from 'es-toolkit';
import { curry as curryCompat } from 'es-toolkit/compat';
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('es-toolkit/compat/curry', () => {
curryCompat(fn)(1)('a')(true);
});
bench('lodash/curry', () => {
curryLodash(fn)(1)('a')(true);
});
});
describe('curry - compat', () => {
const fn = (a: number, b: string, c: boolean) => ({ a, b, c });
bench('es-toolkit/compat/curry', () => {
curryCompat(fn)(1, 'a', true);
});
bench('lodash/curry', () => {
curryLodash(fn)(1, 'a', true);
});
});
describe('curry - compat with placeholder', () => {
const fn = (a: number, b: string, c: boolean) => ({ a, b, c });
bench('es-toolkit/compat/curry', () => {
curryCompat(fn)(1, curryCompat.placeholder, true)('a');
});
bench('lodash/curry', () => {
curryLodash(fn)(1, curryLodash.placeholder, true)('a');
});
});

View File

@ -45,3 +45,58 @@ 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);
```
## Lodash Compatibility
Import `curry` from `es-toolkit/compat` for full compatibility with lodash.
### Signature
```typescript
function curry(
func: (...args: any[]) => any,
arity: number = func.length,
guard?: unknown
): ((...args: any[]) => any) & { placeholder: typeof curry.placeholder };
namespace curry {
placeholder: symbol;
}
```
- `curry` accepts an additional numeric parameter, `arity`, which specifies the number of arguments the function should accept.
- Defaults to the `length` property of the function. If `arity` is negative or `NaN`, it will be converted to `0`. If it's a fractional number, it will be rounded down to the nearest integer.
- `guard` enables use as an iteratee for methods like `Array#map`.
- The `curry.placeholder` value, which defaults to a `symbol`, may be used as a placeholder for partially applied arguments.
- Unlike the native `curry`, this function allows multiple arguments to be called at once and returns a new function that accepts the remaining arguments.
### Examples
```typescript
import { curry } from 'es-toolkit/compat';
const abc = function (a, b, c) {
return Array.from(arguments);
};
let curried = curry(abc);
curried(1)(2)(3);
// => [1, 2, 3]
curried(1, 2)(3);
// => [1, 2, 3]
curried(1, 2, 3);
// => [1, 2, 3]
// Curried with placeholders.
curried(1)(curry.placeholder, 3)(2);
// => [1, 2, 3]
// Curried with arity.
curried = curry(abc, 2);
curried(1)(2);
// => [1, 2]
```

View File

@ -44,3 +44,58 @@ 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);
```
## Lodash 兼容性
`es-toolkit/compat` 中导入 `curry` 以实现与 lodash 的完全兼容。
### 签名
```typescript
function curry(
func: (...args: any[]) => any,
arity: number = func.length,
guard?: unknown
): ((...args: any[]) => any) & { placeholder: typeof curry.placeholder };
namespace curry {
placeholder: symbol;
}
```
- `curry` 接受一个额外的数值参数 `arity`,该参数指定了函数的参数数量。
- 默认为 `func.length`,如果 `arity` 为负数或 `NaN`,则会被转换为 `0`。如果它是一个小数,则会被向下取整到最接近的整数。
- `guard` 使其可以用作 `Array#map` 等方法的迭代器。
- `curry.placeholder` 值默认为一个 `symbol`,可以用作部分应用参数的占位符。
- 不像原生的 `curry`,这个方法允许每次使用多个参数调用,并返回一个接受剩余参数的新函数。
### 示例
```typescript
import { curry } from 'es-toolkit/compat';
const abc = function (a, b, c) {
return Array.from(arguments);
};
let curried = curry(abc);
curried(1)(2)(3);
// => [1, 2, 3]
curried(1, 2)(3);
// => [1, 2, 3]
curried(1, 2, 3);
// => [1, 2, 3]
// 使用占位符进行柯里化
curried(1)(curry.placeholder, 3)(2);
// => [1, 2, 3]
// 指定参数数量
curried = curry(abc, 2);
curried(1)(2);
// => [1, 2]
```

View File

@ -0,0 +1,159 @@
import { describe, it, expect } from 'vitest';
import { curry } from './curry';
import { bind } from './bind';
import { partial } from '../../function/partial';
import { partialRight } from '../../function/partialRight';
describe('curry', () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function fn(_a: unknown, _b: unknown, _c: unknown, _d: unknown) {
// eslint-disable-next-line prefer-rest-params
return Array.from(arguments);
}
it('should curry based on the number of arguments given', () => {
const curried = curry(fn),
expected = [1, 2, 3, 4];
expect(curried(1)(2)(3)(4)).toEqual(expected);
expect(curried(1, 2)(3, 4)).toEqual(expected);
expect(curried(1, 2, 3, 4)).toEqual(expected);
});
it('should allow specifying `arity`', () => {
const curried = curry(fn, 3),
expected = [1, 2, 3];
expect(curried(1)(2, 3)).toEqual(expected);
expect(curried(1, 2)(3)).toEqual(expected);
expect(curried(1, 2, 3)).toEqual(expected);
});
it('should coerce `arity` to an integer', () => {
const values = ['0', 0.6, 'xyz'],
expected = values.map(() => []);
// @ts-expect-error - unusual arity type
const actual = values.map(arity => curry(fn, arity)());
expect(actual).toEqual(expected);
// @ts-expect-error - unusual arity type
expect(curry(fn, '2')(1)(2)).toEqual([1, 2]);
});
it('should support placeholders', () => {
const curried = curry(fn),
ph = curried.placeholder;
expect(curried(1)(ph, 3)(ph, 4)(2)).toEqual([1, 2, 3, 4]);
expect(curried(ph, 2)(1)(ph, 4)(3)).toEqual([1, 2, 3, 4]);
expect(curried(ph, ph, 3)(ph, 2)(ph, 4)(1)).toEqual([1, 2, 3, 4]);
expect(curried(ph, ph, ph, 4)(ph, ph, 3)(ph, 2)(1)).toEqual([1, 2, 3, 4]);
});
it('should persist placeholders', () => {
const curried = curry(fn),
ph = curried.placeholder,
actual = curried(ph, ph, ph, 'd')('a')(ph)('b')('c');
expect(actual).toEqual(['a', 'b', 'c', 'd']);
});
it('should provide additional arguments after reaching the target arity', () => {
const curried = curry(fn, 3);
expect(curried(1)(2, 3, 4)).toEqual([1, 2, 3, 4]);
expect(curried(1, 2)(3, 4, 5)).toEqual([1, 2, 3, 4, 5]);
expect(curried(1, 2, 3, 4, 5, 6)).toEqual([1, 2, 3, 4, 5, 6]);
});
it('should create a function with a `length` of `0`', () => {
const curried = curry(fn);
expect(curried.length).toBe(0);
expect(curried(1).length).toBe(0);
expect(curried(1, 2).length).toBe(0);
const curried2 = curry(fn, 4);
expect(curried2.length).toBe(0);
expect(curried2(1).length).toBe(0);
expect(curried2(1, 2).length).toBe(0);
});
it('should ensure `new curried` is an instance of `func`', () => {
function Foo(value: unknown) {
return value && object;
}
const curried = curry(Foo);
const object = {};
// @ts-expect-error - curried is a constructor
expect(new curried(false) instanceof Foo);
// @ts-expect-error - curried is a constructor
expect(new curried(true)).toBe(object);
});
it('should use `this` binding of function', () => {
const fn = function (this: any, a: string | number, b: string | number, c: string | number) {
const value = this || {};
return [value[a], value[b], value[c]];
};
const object: any = { a: 1, b: 2, c: 3 },
expected = [1, 2, 3];
expect(curry(bind(fn, object), 3)('a')('b')('c')).toEqual(expected);
expect(curry(bind(fn, object), 3)('a', 'b')('c')).toEqual(expected);
expect(curry(bind(fn, object), 3)('a', 'b', 'c')).toEqual(expected);
expect(bind(curry(fn), object)('a')('b')('c')).toEqual(Array(3));
expect(bind(curry(fn), object)('a', 'b')('c')).toEqual(Array(3));
expect(bind(curry(fn), object)('a', 'b', 'c')).toEqual(expected);
object.curried = curry(fn);
expect(object.curried('a')('b')('c')).toEqual(Array(3));
expect(object.curried('a', 'b')('c')).toEqual(Array(3));
expect(object.curried('a', 'b', 'c')).toEqual(expected);
});
it('should work with partialed methods', () => {
const curried = curry(fn),
expected = [1, 2, 3, 4];
const a = partial(curried, 1),
b = bind(a, null, 2),
c = partialRight(b, 4),
d = partialRight(b(3), 4);
expect(c(3)).toEqual(expected);
expect(d()).toEqual(expected);
});
it(`\`curry\` should work for function names that shadow those on \`Object.prototype\``, () => {
const curried = curry(function hasOwnProperty(a: unknown, b: unknown) {
return [a, b];
});
expect(curried(1)(2)).toEqual([1, 2]);
});
it(`\`curry\` should work as an iteratee for methods like \`map\``, () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function fn(_a: unknown, _b: unknown) {
// eslint-disable-next-line prefer-rest-params
return Array.from(arguments);
}
const array = [fn, fn, fn];
// TODO test object like this
// const object = { 'a': fn, 'b': fn, 'c': fn };
[array].forEach(collection => {
const curries = collection.map(curry),
expected = collection.map(() => ['a', 'b']);
const actual = curries.map(curried => curried('a')('b'));
expect(actual).toEqual(expected);
});
});
});

View File

@ -0,0 +1,122 @@
/**
* Creates a function that accepts arguments of `func` and either invokes `func` returning its result, if at least `arity` number of arguments have been provided, or returns a function that accepts the remaining `func` arguments, and so on.
* The arity of `func` may be specified if `func.length` is not sufficient.
*
* The `curry.placeholder` value, which defaults to a `symbol`, may be used as a placeholder for partially applied arguments.
*
* Note: This method doesn't set the `length` property of curried functions.
*
* @param {(...args: any[]) => any} func - The function to curry.
* @param {number=func.length} arity - The arity of func.
* @param {unknown} guard - Enables use as an iteratee for methods like `Array#map`.
* @returns {((...args: any[]) => any) & { placeholder: typeof curry.placeholder }} - Returns the new curried function.
*
* @example
* const abc = function(a, b, c) {
* return Array.from(arguments);
* };
*
* let curried = curry(abc);
*
* curried(1)(2)(3);
* // => [1, 2, 3]
*
* curried(1, 2)(3);
* // => [1, 2, 3]
*
* curried(1, 2, 3);
* // => [1, 2, 3]
*
* // Curried with placeholders.
* curried(1)(curry.placeholder, 3)(2);
* // => [1, 2, 3]
*
* // Curried with arity.
* curried = curry(abc, 2);
*
* curried(1)(2);
* // => [1, 2]
*/
export function curry(
func: (...args: any[]) => any,
arity: number = func.length,
guard?: unknown
): ((...args: any[]) => any) & { placeholder: typeof curry.placeholder } {
arity = guard ? func.length : arity;
arity = Number.parseInt(arity as any, 10);
if (Number.isNaN(arity) || arity < 1) {
arity = 0;
}
const wrapper = function (this: any, ...partials: any[]) {
const holders = replaceHolders(partials);
const length = partials.length - holders.length;
if (length < arity) {
return makeCurry(func, holders, arity - length, partials);
}
if (this instanceof wrapper) {
// @ts-expect-error - fn is a constructor
return new func(...partials);
}
return func.apply(this, partials);
};
wrapper.placeholder = curryPlaceholder;
return wrapper;
}
function makeCurry(
func: (...args: any[]) => any,
holders: any[],
arity: number,
partials: any[]
): ((...args: any[]) => any) & { placeholder: typeof curry.placeholder } {
function wrapper(this: any, ...args: any[]) {
const holdersCount = args.filter(item => item === curry.placeholder).length;
const length = args.length - holdersCount;
args = composeArgs(args, partials, holders);
if (length < arity) {
const newHolders = replaceHolders(args);
return makeCurry(func, newHolders, arity - length, args);
}
if (this instanceof wrapper) {
// @ts-expect-error - fn is a constructor
return new func(...args);
}
return func.apply(this, args);
}
wrapper.placeholder = curryPlaceholder;
return wrapper;
}
function replaceHolders(args: any[]): number[] {
const result = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === curry.placeholder) {
result.push(i);
}
}
return result;
}
function composeArgs(args: any[], partials: any[], holders: number[]): any[] {
const result = [...partials];
const argsLength = args.length;
const holdersLength = holders.length;
let argsIndex = -1,
leftIndex = partials.length,
rangeLength = Math.max(argsLength - holdersLength, 0);
while (++argsIndex < holdersLength) {
if (argsIndex < argsLength) {
result[holders[argsIndex]] = args[argsIndex];
}
}
while (rangeLength--) {
result[leftIndex++] = args[argsIndex++];
}
return result;
}
const curryPlaceholder: unique symbol = Symbol('curry.placeholder');
curry.placeholder = curryPlaceholder;

View File

@ -51,6 +51,7 @@ export { rest } from './function/rest.ts';
export { spread } from './function/spread.ts';
export { attempt } from './function/attempt.ts';
export { rearg } from './function/rearg.ts';
export { curry } from './function/curry.ts';
export { get } from './object/get.ts';
export { set } from './object/set.ts';