TypeScript 3 的通用 curry 函数

Posted

技术标签:

【中文标题】TypeScript 3 的通用 curry 函数【英文标题】:Generic curry function with TypeScript 3 【发布时间】:2019-01-22 09:00:59 【问题描述】:

TypeScript 3.0 引入generic rest parameters。

到目前为止,curry 函数必须在 TypeScript 中使用 finite number of function overloads 和一系列查询实现中传递参数数量的条件语句进行注释。

我希望通用休息参数最终提供实现完全通用解决方案所需的机制。

我想知道如何使用这个新的语言特性来编写一个通用的curry 函数……当然可以假设!

使用我从solution I found on hackernoon 稍微修改的其余参数的 JS 实现看起来像这样:

function curry(fn) 
  return (...args) => 
    if (args.length === 0) 
      throw new Error("Empty invocation")
     else if (args.length < fn.length) 
      return curry(fn.bind(null, ...args))
     else 
      return fn(...args)
    
  

使用通用的 rest 参数和函数重载,我在 TypeScript 中注释这个 curry 函数的尝试如下所示:

interface CurriedFunction<T extends any[], R> 
  (...args: T): void // Function that throws error when zero args are passed
  (...args: T): CurriedFunction<T, R> // Partially applied function
  (...args: T): R // Fully applied function


function curry<T extends any[], R>(
  fn: CurriedFunction<T, R>
): CurriedFunction<T, R> 
  return (...args: T) => 
    if (args.length === 0) 
      throw new Error("Empty invocation")
     else if (args.length < fn.length) 
      return curry(fn.bind(null, ...args))
     else 
      return fn(...args)
    
  

但是 TypeScript 会抛出错误:

Type 'CurriedFunction<any[], >' is not assignable to type 'CurriedFunction<T, R>'.
Type '' is not assignable to type 'R'.

我不明白R 在哪里以及为什么被推断为

【问题讨论】:

不确定 CurriedFunction 类型 args 的柯里化应该如何发生,因为 T 参数到 CurriedFunction 在输入和输出中总是相同的 你是对的。我有一种感觉,我需要另一个从 any[] 扩展的通用变量来注释返回的 curried 函数的参数。 强类型 bind() 在 TypeScript 3.0 中仍然不可能,因为在类型系统中没有(支持的)concatenate tuples 方法。所以你可能不会轻易做到这一点。 @jcalz 连接是不可能的我同意,但是删除第一个参数(或前 n 个参数)应该是可能的,这对于 bind 还不够吗? (顺便说一句:恭喜你获得金牌 :)) 不可能删除泛型 n 的前 n 个参数,这是需要的。你仍然可以做大量的重载或偷偷摸摸的编译器恶意递归,但这不是很好。 【参考方案1】:

使用当前版本的 typescript,可以创建一个相对简单且类型正确的泛型 curry 函数。

type CurryFirst<T> = T extends (x: infer U, ...rest: any) => any ? U : never;
type CurryRest<T> =
    T extends (x: infer U) => infer V ? U :
    T extends (x: infer U, ...rest: infer V) => infer W ? Curried<(...args: V) => W> :
    never

type Curried<T extends (...args: any) => any> = (x: CurryFirst<T>) => CurryRest<T>

const curry = <T extends (...args: any) => any>(fn: T): Curried<T> => 
    if (!fn.length)  return fn(); 
    return (arg: CurryFirst<T>): CurryRest<T> => 
        return curry(fn.bind(null, arg) as any) as any;
    ;


describe("Curry", () => 
    it("Works", () => 
        const add = (x: number, y: number, z: number) => x + y + z;
        const result = curry(add)(1)(2)(3)
        result.should.equal(6);
    );
);

这基于两个类型构造函数:

CurryFirst 将给函数返回该函数的第一个参数的类型。 CurryRest 将返回应用了第一个参数的 curried 函数的返回类型。特殊情况是当函数类型T 只接受一个参数时,那么CurryRest&lt;T&gt; 将只返回函数类型T 的返回类型

基于这两个,T 类型函数的柯里化版本的类型签名简单地变成:

Curried<T> = (arg: CurryFirst<T>) => CurryRest<T>

我在这里做了一些简单的限制:

您不想对无参数函数进行柯里化。您可以轻松添加它,但我认为它没有意义。 我不保留this 指针。这对我来说也没有意义,因为我们在这里进入了纯 FP 领域。

如果 curry 函数将参数累积在一个数组中,并执行一次 fn.apply,而不是多次 fn.bind 调用,则可以提高推测性能。但是必须注意确保可以多次正确调用部分应用的函数。

【讨论】:

试验和错误表明这不允许像 curry(add)(1, 2)(3) 这样的调用,但在像我这样 lodash 的 curry 实现工作但不完全的情况下,这可以作为一个很好的插入模块声明在类型部门剪掉它。 curry(add)(1, 2)(3) 不应该工作,但 curry(add)(1, 2) 应该。这很容易在代码中修复。只需将返回函数更改为return (...args) =&gt; curry(fn.bind(null, ...args)),但类型错误。如果所有参数都存在,则这相当于fn.apply,因此您不必考虑部分应用的函数可以正确调用多次。一个简单的例子是Mostly Adequate's curry definition。 非常好,但可以让curry(add)(1, 2, 3)curry(add)(1, 2)(3)curry(add)(1)(2)(3) 的所有人都能使用? 要让CurryRest&lt;T&gt; 处理具有单个参数的函数,我必须将T extends (x: infer U) =&gt; infer V ? U ... 更改为T extends (x: infer U) =&gt; infer V ? () =&gt; V ...【参考方案2】:

目前,正确输入此内容的最大障碍是 TypeScript 从 TypeScript 3.0 开始无法连接或拆分元组。有suggestions 可以做到这一点,TypeScript 3.1 及更高版本可能正在开发中,但现在还没有。到今天为止,您所能做的就是枚举最大有限长度的案例,或者尝试trick the compiler into using recursion,即not recommended。

如果我们想象有一个TupleSplit&lt;T extends any[], L extends number&gt; 类型的函数可以接受一个元组和一个长度,并将该长度的元组拆分为初始部分和其余部分,这样TupleSplit&lt;[string, number, boolean], 2&gt; 就会产生init: [string, number], rest: [boolean],那么您可以将 curry 函数的类型声明为:

declare function curry<A extends any[], R>(
  f: (...args: A) => R
): <L extends TupleSplit<A, number>['init']>(
    ...args: L
  ) => 0 extends L['length'] ?
    never :
    ((...args: TupleSplit<A, L['length']>['rest']) => R) extends infer F ?
    F extends () => any ? R : F : never;

为了能够尝试,让我们介绍一个仅适用于 L3TupleSplit&lt;T, L&gt; 版本(如果需要,您可以添加)。它看起来像这样:

type TupleSplit<T extends any[], L extends number, F = (...a: T) => void> = [
   init: [], rest: T ,
  F extends ((a: infer A, ...z: infer Z) => void) ?
   init: [A], rest: Z  : never,
  F extends ((a: infer A, b: infer B, ...z: infer Z) => void) ?
   init: [A, B], rest: Z  : never,
  F extends ((a: infer A, b: infer B, c: infer C, ...z: infer Z) => void) ?
   init: [A, B, C], rest: Z  : never,
  // etc etc for tuples of length 4 and greater
  ... init: T, rest: [] []
][L];

现在我们可以在类似的函数上测试curry 的声明

function add(x: number, y: number) 
  return x + y;

const curriedAdd = curry(add);

const addTwo = curriedAdd(2); // (y: number) => number;
const four = curriedAdd(2,2); // number
const willBeAnError = curriedAdd(); // never

这些类型在我看来是正确的。


当然,这并不意味着curry实现 会对这种类型感到满意。您也许可以像这样实现它:

return <L extends TupleSplit<A, number>['init']>(...args: TupleSplit<A, L['length']>['rest']) => 
  if (args.length === 0) 
    throw new Error("Empty invocation")
   else if (args.length < f.length) 
    return curry(f.bind(null, ...args))
   else 
    return f(...args as A)
  

可能。我还没有测试过。

无论如何,希望这有一定的意义,并给你一些方向。祝你好运!


更新

我没有注意到 curry() 会返回更多的柯里化函数,如果你不传递所有参数的话。这样做需要递归类型,如下所示:

type Curried<A extends any[], R> =
  <L extends TupleSplit<A, number>['init']>(...args: L) =>
    0 extends L['length'] ? never :
    0 extends TupleSplit<A, L['length']>['rest']['length'] ? R :
    Curried<TupleSplit<A,L['length']>['rest'], R>;

declare function curry<A extends any[], R>(f: (...args: A)=>R): Curried<A, R>;

function add(x: number, y: number) 
  return x + y;

const curriedAdd = curry(add);

const addTwo = curriedAdd(2); // Curried<[number], number>
const three = addTwo(1); // number
const four = curriedAdd(2,2); // number
const willBeAnError = curriedAdd(); // never

这更像是原始定义。


但我也注意到,如果你这样做:

const wat = curriedAdd("no error?"); // never

它没有得到错误,而是返回never。这对我来说看起来像是一个编译器错误,但我还没有跟进它。编辑:好的,我为此提交了Microsoft/TypeScript#26491。

干杯!

【讨论】:

L['length'] 的酷用法要记住那个:) Wowsers 非常感谢@jcalz — 我显然需要提高我的 TS 技能! 0 extends L['length'] 是什么意思(@jcalz)?这让我大吃一惊。我在哪里可以阅读该表达式? X extends Y ? T : U 语法称为conditional type(?: 也是子句的一部分,而不仅仅是extends 部分)。如果L 是具有名为length 的属性的类型(例如元组),那么L['length'] 就是该属性的类型。该符号称为lookup type。所以0 extends L['length'] 是条件类型的一部分,检查L 是否是零长度元组。 这是一个非常优雅的解决方案。但是,一个缺点是它丢失了参数名称,从而降低了 IntelliSense 输出的用处。是否有保留参数名称或以其他方式产生更好输出的解决方案?【参考方案3】:

这里最大的问题是您试图定义一个具有可变数量“咖喱级别”的通用函数——例如a =&gt; b =&gt; c =&gt; dx =&gt; y =&gt; z(k, l) =&gt; (m, n) =&gt; o,其中所有这些函数都以某种方式由相同(尽管是通用)类型定义 F&lt;T, R&gt; 表示——这在 TypeScript 中是不可能的,因为你不能任意拆分 @ 987654325@ 分成两个较小的元组...

从概念上讲,您需要:

FN<A extends any[], R> = (...a: A) => R | (...p: A.Prefix) => FN<A.Suffix, R>

TypeScript AFAIK 无法做到这一点。

最好的办法是使用一些可爱的重载:

FN1<A, R>             = (a: A) => R
FN2<A, B, R>          = ((a: A, b: B) => R)             | ((a: A) => FN1<B, R>)
FN3<A, B, C, R>       = ((a: A, b: B, c: C) => R)       | ((a: A, b: B) => FN1<C, R>)       | ((a: A) => FN2<B, C, R>)
FN4<A, B, C, D, R>    = ((a: A, b: B, c: C, d: D) => R) | ((a: A, b: B, c: C) => FN1<D, R>) | ((a: A, b: B) => FN2<C, D, R>) | ((a: A) => FN3<B, C, D, R>)

function curry<A, R>(fn: (A) => R): FN1<A, R>
function curry<A, B, R>(fn: (A, B) => R): FN2<A, B, R>
function curry<A, B, C, R>(fn: (A, B, C) => R): FN3<A, B, C, R>
function curry<A, B, C, D, R>(fn: (A, B, C, D) => R): FN4<A, B, C, D, R>

许多语言都有像这些内嵌的展开类型,因为很少有类型系统在定义类型时支持这种级别的递归流控制。

【讨论】:

以上是关于TypeScript 3 的通用 curry 函数的主要内容,如果未能解决你的问题,请参考以下文章

如何在 TypeScript 3+ 中正确键入通用元组剩余参数?

返回函数的通用 TypeScript 类型,将 ReturnType 替换为返回函数的 ReturnType

TypeScript 通用函数 - 为数组应用函数时出现“参数不可分配错误”

Typescript - 具有通用类型函数的索引签名

TypeScript - 通用函数的 ReturnType 不起作用

TypeScript 缩短通用函数调用