如何在 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] 和有效,并推断 Sstring).

好的,使用并实现它:

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.

这些对我来说是正确的错误。


Playground link to code