如何在 Typescript 4 中编写 curry 和 compose?
How to write curry and compose in Typescript 4?
看完variadic types我想做这个,但我想知道如何使用函数数组。
这是我的第一次尝试:
function curry<T extends any[]>(fn: (...args: T) => any) {
return function(...args: T) {
return args.length >= fn.length
? fn(...args)
: curry(fn.bind(undefined, ...args));
}
}
但是对于 fn.bind
我得到“'this' 类型上下文 '(...args: T) => any' 不可分配给方法的 'this'输入'(this: undefined, ...args: any[]) => any'。"
有什么想法吗?
您并没有真正在代码中使用 variadic tuple tpes。您正在实现的 curry()
的特定风格是采用部分参数列表,然后可能 returns 另一个采用列表的部分剩余部分的函数。这意味着参数 T
的初始元组可能会分成许多不同的部分,因此不会像文档中的 partialCall()
函数那样从上下文中自动推断出来。
相反,您需要明确地将 T
元组分解为可能的子元组。让我们来表示 curry()
的所需输出类型,它采用一个函数,其参数是元组 A
并且 return 类型是 R
:
type Curried<A extends any[], R> =
<P extends Partial<A>>(...args: P) => P extends A ? R :
A extends [...SameLength<P>, ...infer S] ? S extends any[] ? Curried<S, R>
: never : never;
type SameLength<T extends any[]> = Extract<{ [K in keyof T]: any }, any[]>
让我把它翻译成英文。 Curried<A, R>
是一个泛型函数,它的参数必须是某种元组类型 P
并且被限制为 Partial<A>
.
对于元组,Partial<A>
结束意味着你可以省略元组的任何后缀(从某个地方到结尾)。所以 [1, 2, 3]
可以分配给 Partial<[1,2,3,4,5,6,7]>
,但 [1, 2, 4]
不能。 undefined
有点问题,因为 [1, undefined, 3]
也可以分配给 Partial<[1,2,3,4,5,6,7]>
,但我将忽略它,如果它变得重要,它可以解决。无论如何,这意味着 Curried<A, R>
的参数必须是 A
元组的某个前缀(初始块)。
Curried<A, R>
的return类型取决于传入的前缀P
。如果P
是整个元组A
,则return 类型只是 R
(这就是当您最终为函数提供所有参数时发生的情况)。否则,您将 A
拆分为前缀 P
及其后缀 S
,以及 return 类型为 Curried<S, R>
.
的新柯里化函数
将 A
拆分为 [...SameLength<P>, ...infer S]
使用可变元组类型。请注意,SameLength<P>
只是一个与 P
长度相同但元素类型与 any
相同的元组。这避免了 P
被推断为非常窄的问题(比如 A
是 [number, number, number]
然后 P
是 [0, 0]
。你不能拆分 [number, number, string]
变成 [0, 0, ...infer S]
因为 number
不能分配给 0
。但是我们只关心这里的长度,所以我们把 [number, number, string]
分成 [any, any, ...infer S]
和有效,并推断 S
为 string
).
好的,使用并实现它:
function curry<A extends any[], R>(fn: (...args: A) => R): Curried<A, R> {
return (...args: any[]): any =>
args.length >= fn.length ? fn(...args as any) : curry((fn as any).bind(undefined, ...args));
}
我在 curry()
的实现中使用了很多 type assertions,因为编译器几乎无可救药地试图验证 returned 函数是否可以分配给Curried<A, R>
。与其担心它,我只是告诉编译器不要费心验证安全性,并自己负责使实现正确。 (如果错了,怪我自己,不怪编译器)。
好的,我们开始吧。有用吗?
const fn = (a: string, b: number, c: boolean) => (a.length <= b) === c ? "yep" : "nope";
const cFn = curry(fn);
const val1 = cFn("")(1)(true);
console.log(val1); // yep
const val2 = cFn("", 1, true);
console.log(val2); // yep
const val3 = cFn()()()()("", 1)()()(true); // yep
我觉得不错。请注意,根据我的定义,如果您不带参数调用 Curried<A, R>
,您只会返回 Curried<A, R>
。这里有一些故意的错误,因此您可以看到编译器捕获了它们:
// errors
cFn(1, 1, true); // error!
// ~ <-- not a string
cFn("", 1, true, false); // error!
// ~~~~~ <-- Expected 0-3 arguments, but got 4
cFn("")(1)(false)(true); // error!
//~~~~~~~~~~~~~~~ <-- This expression is not callable.
这些对我来说是正确的错误。
看完variadic types我想做这个,但我想知道如何使用函数数组。
这是我的第一次尝试:
function curry<T extends any[]>(fn: (...args: T) => any) {
return function(...args: T) {
return args.length >= fn.length
? fn(...args)
: curry(fn.bind(undefined, ...args));
}
}
但是对于 fn.bind
我得到“'this' 类型上下文 '(...args: T) => any' 不可分配给方法的 'this'输入'(this: undefined, ...args: any[]) => any'。"
有什么想法吗?
您并没有真正在代码中使用 variadic tuple tpes。您正在实现的 curry()
的特定风格是采用部分参数列表,然后可能 returns 另一个采用列表的部分剩余部分的函数。这意味着参数 T
的初始元组可能会分成许多不同的部分,因此不会像文档中的 partialCall()
函数那样从上下文中自动推断出来。
相反,您需要明确地将 T
元组分解为可能的子元组。让我们来表示 curry()
的所需输出类型,它采用一个函数,其参数是元组 A
并且 return 类型是 R
:
type Curried<A extends any[], R> =
<P extends Partial<A>>(...args: P) => P extends A ? R :
A extends [...SameLength<P>, ...infer S] ? S extends any[] ? Curried<S, R>
: never : never;
type SameLength<T extends any[]> = Extract<{ [K in keyof T]: any }, any[]>
让我把它翻译成英文。 Curried<A, R>
是一个泛型函数,它的参数必须是某种元组类型 P
并且被限制为 Partial<A>
.
对于元组,Partial<A>
结束意味着你可以省略元组的任何后缀(从某个地方到结尾)。所以 [1, 2, 3]
可以分配给 Partial<[1,2,3,4,5,6,7]>
,但 [1, 2, 4]
不能。 undefined
有点问题,因为 [1, undefined, 3]
也可以分配给 Partial<[1,2,3,4,5,6,7]>
,但我将忽略它,如果它变得重要,它可以解决。无论如何,这意味着 Curried<A, R>
的参数必须是 A
元组的某个前缀(初始块)。
Curried<A, R>
的return类型取决于传入的前缀P
。如果P
是整个元组A
,则return 类型只是 R
(这就是当您最终为函数提供所有参数时发生的情况)。否则,您将 A
拆分为前缀 P
及其后缀 S
,以及 return 类型为 Curried<S, R>
.
将 A
拆分为 [...SameLength<P>, ...infer S]
使用可变元组类型。请注意,SameLength<P>
只是一个与 P
长度相同但元素类型与 any
相同的元组。这避免了 P
被推断为非常窄的问题(比如 A
是 [number, number, number]
然后 P
是 [0, 0]
。你不能拆分 [number, number, string]
变成 [0, 0, ...infer S]
因为 number
不能分配给 0
。但是我们只关心这里的长度,所以我们把 [number, number, string]
分成 [any, any, ...infer S]
和有效,并推断 S
为 string
).
好的,使用并实现它:
function curry<A extends any[], R>(fn: (...args: A) => R): Curried<A, R> {
return (...args: any[]): any =>
args.length >= fn.length ? fn(...args as any) : curry((fn as any).bind(undefined, ...args));
}
我在 curry()
的实现中使用了很多 type assertions,因为编译器几乎无可救药地试图验证 returned 函数是否可以分配给Curried<A, R>
。与其担心它,我只是告诉编译器不要费心验证安全性,并自己负责使实现正确。 (如果错了,怪我自己,不怪编译器)。
好的,我们开始吧。有用吗?
const fn = (a: string, b: number, c: boolean) => (a.length <= b) === c ? "yep" : "nope";
const cFn = curry(fn);
const val1 = cFn("")(1)(true);
console.log(val1); // yep
const val2 = cFn("", 1, true);
console.log(val2); // yep
const val3 = cFn()()()()("", 1)()()(true); // yep
我觉得不错。请注意,根据我的定义,如果您不带参数调用 Curried<A, R>
,您只会返回 Curried<A, R>
。这里有一些故意的错误,因此您可以看到编译器捕获了它们:
// errors
cFn(1, 1, true); // error!
// ~ <-- not a string
cFn("", 1, true, false); // error!
// ~~~~~ <-- Expected 0-3 arguments, but got 4
cFn("")(1)(false)(true); // error!
//~~~~~~~~~~~~~~~ <-- This expression is not callable.
这些对我来说是正确的错误。