16

TypeScript 3.0 引入了通用的剩余参数

到目前为止,curry函数必须在 TypeScript 中使用有限数量的函数重载和一系列条件语句来查询实现中传递的参数的数量。

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

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

使用我在hackernoon上找到的解决方案稍微修改的使用rest params的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被推断为{}

4

3 回答 3

11

现在,正确键入此内容的最大障碍是 TypeScript 无法从 TypeScript 3.0 开始连接或拆分元组。有这样做的建议,TypeScript 3.1 及更高版本可能正在开发中,但现在还没有。到今天为止,您所能做的就是枚举最大有限长度的案例,或者尝试欺骗编译器使用推荐的递归。

如果我们想象有一个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;

为了能够尝试,让我们介绍一个TupleSplit<T, L>仅适用于L最多的版本3(您可以根据需要添加)。它看起来像这样:

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

干杯!

于 2018-08-15T14:31:11.437 回答
6

使用当前版本的 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<T>将只返回函数类型的返回类型T

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

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

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

  • 您不想使用无参数函数。你可以很容易地添加它,但我认为它没有意义。
  • 我不保留this指针。这对我来说也没有意义,因为我们在这里进入了纯 FP 领域。

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

于 2020-04-12T12:14:11.193 回答
1

这里最大的问题是你试图定义一个具有可变数量的“柯里化级别”的泛型函数——例如a => b => c => dor x => y => zor (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>

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

于 2018-08-15T15:36:37.423 回答