如何在 Typescript 4 中编写咖喱和撰写?

mja*_*kic 1 functional-programming currying composition typescript

在经历了可变参数类型之后,我想做这个,但我想知道如何使用函数数组。

这是我的第一次尝试:

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));
  }
}
Run Code Online (Sandbox Code Playgroud)

但是因为fn.bind我得到“'(...args: T) => any' 类型的'this' 上下文不能分配给'(this: undefined, ...args: any[ ]) => 任何'。”

有任何想法吗?

jca*_*alz 8

您并没有真正在代码中使用可变参数元组 tpescurry()您正在实现的特定风格是采用部分参数列表,然后可能返回另一个采用列表的部分剩余部分的函数。这意味着参数的初始元组T可能被分成许多不同的部分,因此这不会partialCall()像文档中的函数那样从上下文中自动推断出来。

相反,您需要明确地将T元组分解为可能的子元组。让我们表示curry()需要一个函数的所需输出的类型,该函数的参数是元组A,其返回类型是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[]>
Run Code Online (Sandbox Code Playgroud)

让我把它翻译成英文。ACurried<A, R>是一个泛型函数,其参数必须是某种P限制为 的元组类型Partial<A>

对于元组,Partial<A>最终意味着您可以省略元组的任何后缀(从某个地方到最后)。So[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>取决于P传入的前缀。如果P是整个 tuple A,则返回类型是 just R(当您最终为函数提供所有参数时会发生这种情况)。否则,您将拆分A为前缀P及其后缀S,并返回一个新的柯里化函数类型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));
}
Run Code Online (Sandbox Code Playgroud)

我在 的实现中使用了大量类型断言curry(),因为编译器在尝试验证返回的函数是否可赋值给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
Run Code Online (Sandbox Code Playgroud)

在我看来很好。请注意,根据我的定义,如果您Curried<A, R>不带参数调用 a ,您只会返回 a 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.
Run Code Online (Sandbox Code Playgroud)

这些在我看来是正确的错误。


Playground 链接到代码