mirror of
https://github.com/toss/es-toolkit.git
synced 2024-11-24 03:32:58 +03:00
feat(compat): implement curry (#535)
* feat(compat): implement curry * make lint happy * doc
This commit is contained in:
parent
9bb1203b51
commit
02ce5e9024
@ -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);
|
||||
});
|
||||
});
|
44
benchmarks/performance/curry.bench.ts
Normal file
44
benchmarks/performance/curry.bench.ts
Normal 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');
|
||||
});
|
||||
});
|
@ -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]
|
||||
```
|
||||
|
@ -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]
|
||||
```
|
||||
|
159
src/compat/function/curry.spec.ts
Normal file
159
src/compat/function/curry.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
122
src/compat/function/curry.ts
Normal file
122
src/compat/function/curry.ts
Normal 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;
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user