【问题标题】:Is it possible to implement a TypeScript generic version of Y-combinator?是否可以实现 Y-combinator 的 TypeScript 通用版本?
【发布时间】:2021-09-07 23:23:52
【问题描述】:

Y-combinator JavaScript 实现:

const Y = g => (x => g(x(x)))(x => g(x(x)))
//or
const Y = f => {
  const g = x => f(x(x))
  return g(g)
}

我只是好奇,是否可以实现 Y-combinator 的 TypeScript 通用版本?

type Y<F> = ???

【问题讨论】:

  • 应该注意的是,Y 组合器的这种实现实际上并不工作(出于任何实际目的),因为 javascript 不会被延迟评估;一旦达到堆栈限制,它总是会无限递归然后出错。更实用的版本是 Z 组合器,它显式定义了一个参数,允许递归停止:const Z = f =&gt; (x =&gt; f(v =&gt; x(x)(v)))(x =&gt; f(v =&gt; x(x)(v)))

标签: typescript


【解决方案1】:

简短的回答:

输入为单参数函数的定点组合器的 TypeScript 类型为:

type Y = &lt;I, O&gt;(F: (f: (i: I) =&gt; O) =&gt; ((i: I) =&gt; O)) =&gt; (i: I) =&gt; O;

长答案如下:


fixed point combinator 的类型应该是以下形式的泛型函数:

type Fixed = <T>(f: (t: T) => T) => T;

所以如果你有一个fixed 类型为Fixed 的值,你应该能够在任何一个输出与输入类型相同的参数函数上调用它,并且(神奇地?)得到一个结果是该函数的一个不动点:

console.log(fixed(Math.cos)) // 0.739085...

对于某些行为良好的函数f,您可以通过从一些随机值开始并迭代f 很多次来获得类似的行为:

const fixed: Fixed = f => {
  let r = null as any;
  for (let i = 0; i < 1000; i++) r = f(r);
  return r;
}

这是一个愚蠢的实现(或者至少不是经典的实现),但它对于查找输入本身就是函数的函数的固定点的正常用例来说已经足够了。

例如,我们定义一个protoFactorial函数,其不动点通过递归计算非负整数的factorial

const protoFactorial = (f: (n: number) => number) =>
  (n: number) => n === 0 ? 1 : n * f(n - 1)

请注意,输入f(n: number) =&gt; number 类型的函数,输出是该类型的另一个函数,其输入nn 是我们要计算其阶乘的数字。如果n0,则返回1;否则返回n * f(n - 1)

如果我们有一个不动点组合器fixed,我们应该能够得到所需的阶乘函数,如下所示:

const factorial = fixed(protoFactorial);
console.log(factorial(7)); // 5040

上面的fixed 确实适用于此。


但是对于 Y 组合器,我们倾向于要求它所操作的类型 T 本身就是一个函数类型(尽管在无类型的 lambda 演算中并没有真正的区别),因此我倾向于给Y一个泛型类型,具体取决于T函数类型的输入和输出:

type Y = <I, O>(F: (f: (i: I) => O) => ((i: I) => O)) => (i: I) => O;

如果将T 替换为(i: I) =&gt; O,则类似于Fixed。无论如何,对于Y 类型的值Y,我们应该能够调用Y(protoFactorial) 并获得正确的factorial

这是Y 在 TypeScript 中的实现:

interface Ω<T> {
  (x: Ω<T>): T;
}

const Y: Y = <I, O>(F: (f: (i: I) => O) => (i: I) => O) =>
  ((x: Ω<(i: I) => O>) => F(x(x)))((x: Ω<(i: I) => O>) => F(x(x)));

请注意,为了强类型化 Y 组合器实现,我们需要递归类型 Ω&lt;T&gt;x 参数必须属于这种类型,以便允许像 x(x) 这样的调用编译并生成 (i: I) =&gt; O 类型的输出。

这与您的 g =&gt; (x =&gt; g(x(x)))(x =&gt; g(x(x))) 实现相同,将 g 重命名为 F 并使用类型注释来帮助编译器。


这就是问题的答案,但不幸的是,Y 组合器不能在 JavaScript 中按原样使用,它会在评估函数体之前急切地评估所有函数输入:

try {
  const factorial = Y(protoFactorial);
} catch (e) {
  console.log(e); // too much recursion
}

糟糕,调用Y(protoFactorial) 会急切地扩展到无穷无尽的F(F(F(F(F(F(.... 并爆炸。


为了在 JavaScript 中获得可行的定点组合器,我们需要类似于 Z combinator 的东西,它通过使用 eta 抽象(参见 What is Eta abstraction in lambda calculus used for?)延迟计算 x(x) 以产生 v =&gt; x(x)(v)

const Z: Y = <I, O>(F: (f: (i: I) => O) => (i: I) => O) =>
  ((x: Ω<(i: I) => O>) => F(v => x(x)(v)))((x: Ω<(i: I) => O>) => F(v => x(x)(v)));

现在,如果我们使用 Z 代替 Y 它在运行时可以工作:

const factorial = Z(protoFactorial);
// const factorial: (i: number) => number
console.log(factorial(7)); // 5040

当然,如果我们只关心类型并且我们不需要假装 JavaScript 和 TypeScript 在运行时缺乏一流的递归,那么我们可以递归地实现一个 Y 类型的函数。少麻烦:

const R: Y = f => f(x => R(f)(x))    
console.log(R(protoFactorial)(7)) // 5040

从类型系统来看,fixedYZR都是Y类型的值:

function acceptY(y: Y) { }
acceptY(fixed)
acceptY(Y)
acceptY(Z)
acceptY(R)

因此我们可以相当自信地说,输入为单参数函数的定点组合器的 TypeScript 类型是:

type Y = <I, O>(F: (f: (i: I) => O) => ((i: I) => O)) => (i: I) => O;

Playground link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-04-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-01
    • 2016-06-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多