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<T>
将只返回函数类型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) => 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<T>
处理具有单个参数的函数,我必须将T extends (x: infer U) => infer V ? U ...
更改为T extends (x: infer U) => infer V ? () => V ...
。【参考方案2】:
目前,正确输入此内容的最大障碍是 TypeScript 从 TypeScript 3.0 开始无法连接或拆分元组。有suggestions 可以做到这一点,TypeScript 3.1 及更高版本可能正在开发中,但现在还没有。到今天为止,您所能做的就是枚举最大有限长度的案例,或者尝试trick the compiler into using recursion,即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;
为了能够尝试,让我们介绍一个仅适用于 L
到 3
的 TupleSplit<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()
会返回更多的柯里化函数,如果你不传递所有参数的话。这样做需要递归类型,如下所示:
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 => b => c => d
或 x => y => z
或 (k, l) => (m, n) => o
,其中所有这些函数都以某种方式由相同(尽管是通用)类型定义 F<T, R>
表示——这在 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