【问题标题】:How to write curry and compose in Typescript 4?如何在 Typescript 4 中编写 curry 和 compose?
【发布时间】:2020-09-15 14:25:08
【问题描述】:

经过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,我得到“'(...args: T) => any' 类型的'this' 上下文不可分配给'(this: undefined, . ..args: any[]) => any'。"

有什么想法吗?

【问题讨论】:

    标签: typescript functional-programming composition currying


    【解决方案1】:

    您并没有真正在代码中使用variadic tuple tpes。您正在实现的 curry() 的特定风格是采用部分参数列表,然后可能返回另一个函数,该函数采用列表的部分剩余部分。这意味着参数 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[]>
    

    让我把它翻译成英文。 Curried&lt;A, R&gt; 是一个泛型函数,其参数必须是某种元组类型 P,它被限制为 Partial&lt;A&gt;。

    对于元组,Partial&lt;A&gt; 最终意味着您可以省略元组的任何后缀(从某个地方到结尾)。所以[1, 2, 3] 可以分配给Partial&lt;[1,2,3,4,5,6,7]&gt;,但[1, 2, 4] 不是。 undefined 有一点问题,因为 [1, undefined, 3] 也可分配给 Partial&lt;[1,2,3,4,5,6,7]&gt;,但我将忽略这一点,如果它变得重要,它可以解决。无论如何,这意味着Curried&lt;A, R&gt; 的参数必须是A 元组的某个前缀(初始块)。

    Curried&lt;A, R&gt; 的返回类型取决于传入的前缀P。如果P 是整个元组A,那么返回类型就是R(这就是当你终于给定函数的所有参数)。否则,将A 拆分为前缀P 及其后缀S,并返回Curried&lt;S, R&gt; 类型的新柯里化函数。

    将A 拆分为[...SameLength&lt;P&gt;, ...infer S] 使用可变元组类型。请注意,SameLength&lt;P&gt; 只是一个长度与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,因为编译器几乎绝望地试图验证返回的函数是否可以分配给Curried&lt;A, R&gt;。与其担心它,我只是告诉编译器不要费心验证安全性,并自己负责使实现正确。 (如果错了,我怪我自己,而不是编译器)。

    好的,我们开始吧。有用吗?

    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&lt;A, R&gt;,您只会得到Curried&lt;A, R&gt;。以下是一些故意的错误,因此您可以看到编译器捕获了它们:

    // 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

    【讨论】:

    • 太棒了,非常感谢 - 我只需要十年的时间来写这篇文章。乍一看,我似乎还没有完全了解可变参数类型。
    • 此实现不适用于泛型函数,所有泛型参数都被提升为any。这是打字稿限制吗?
    • 是的,TypeScript 在一些特定场景(如this)之外缺乏对“更高种类的类型”的支持(有关功能请求,请参阅microsoft/TypeScript#1213),因此自动传播类型参数的可能性很小类似Curried.
    猜你喜欢
    • 2018-06-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-30
    • 1970-01-01
    • 2017-01-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多