TypeScript 3 的通用咖喱函数

Generic curry function with TypeScript 3

引入了 TypeScript 3.0 generic rest parameters

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

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

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

我从 solution I found on hackernoon 稍微修改了一下使用 rest 参数的 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)
    }
  }
}

使用通用剩余参数和函数重载,我尝试在 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 在哪里以及为什么被推断为 {}

目前,正确输入此内容的最大障碍是 TypeScript 在 TypeScript 3.0 中无法连接或拆分元组。有suggestions for doing this, and something might be in the works for TypeScript 3.1 and beyond, but it's just not there right now. As of today all you could do is enumerate cases up to some maximum finite length, or try to trick the compiler into using recursion which is not recommended.

如果我们想象有一个 TupleSplit<T extends any[], L extends number> 类型的函数,它可以接受一个元组和一个长度,并将该长度的元组拆分为初始组件和其余部分,这样 TupleSplit<[string, number, boolean], 2> 将产生 {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<T, L> 版本(如果需要,您可以添加).它看起来像这样:

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() returns 进一步柯里化函数,如果你不传递所有参数的话。这样做需要一个递归类型,像这样:

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

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

干杯!

这里最大的问题是您试图定义一个具有可变数量 'curried levels' 的通用函数——例如a => b => c => dx => y => z(k, l) => (m, n) => o,其中所有这些函数都以某种方式由相同的(尽管是通用的)类型定义 F<T, R> 表示——这在 TypeScript 中是不可能的因为你不能随意将 generic rests 分成两个较小的元组...

概念上你需要:

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,可以创建一个相对简单且类型正确的通用 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 将给函数 return 该函数的第一个参数的类型。
  • CurryRest 将 return 应用第一个参数的柯里化函数的 return 类型。特殊情况是当函数类型 T 只接受一个参数时,那么 CurryRest<T> 将只是 return 函数类型 T[=37 的 return 类型=]

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

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

我这里做了一些简单的约束:

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

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

目前最好的方法是使用this one:

import { F } from "ts-toolbelt";
type FuncType = (a: number, b: string, c: bigint) => 2
type ExampleCurriedFunction = F.Curry<FuncType>

这里有用link

它将被咖喱化,以便可以部分应用函数,就像在 ramda curry 函数中一样(see here)