【问题标题】:Two-layer "Y-style" combinator. Is this common? Does this have an official name?两层“Y 型”组合器。这很常见吗?这个有官方名称吗?
【发布时间】:2013-04-21 22:02:59
【问题描述】:

我一直在研究禁止 use-before-def 并且没有可变单元格(没有 set!setq)的语言如何提供递归。我当然遇到了(著名的?臭名昭著的?)Y 组合器和朋友,例如:

当我去实现这种风格的“letrec”语义时(也就是说,允许定义一个局部变量,使它可以是一个递归函数,在幕后它永远不会引用它自己的名字) ,我最后写的组合子是这样的:

Y_letrec = λf . (λx.x x) (λs . (λa . (f ((λx.x x) s)) a))

或者,分解出 U 组合子:

U = λx.x x
Y_letrec = λf . U (λs . (λa . (f (U s)) a))

读作:Y_letrec 是一个函数,它接受一个待递归函数ff 必须是接受s 的单参数函数,其中s 是函数 那f可以调用实现自递归。 f 预计定义并返回 执行“真实”操作的“内部”功能。那个内部函数接受 参数a(或者在一般情况下是参数列表,但无法表达 在传统符号中)。调用 Y_letrec 的结果是调用的结果 f,假定它是一个“内部”函数,可以调用。

我这样设置的原因是我可以使用解析树形式的 直接被递归的函数,无需修改,只是额外包装了一个 处理 letrec 时在转换过程中围绕它的功能层。例如,如果 原代码是:

(letrec ((foo (lambda (a) (foo (cdr a))))))

那么转换后的形式将是:

(define foo (Y_letrec (lambda (foo) (lambda (a) (foo (cdr a))))))

请注意,两者的内部函数体是相同的。

我的问题是:

  • 我的 Y_letrec 函数常用吗?
  • 它有一个知名的名字吗?

注意:上面的第一个链接引用了一个与“应用顺序 Y 组合器”类似的函数(在“步骤 5”中),尽管我很难找到该命名的权威来源。

2013 年 4 月 28 日更新:

我意识到上面定义的 Y_letrec 非常接近,但与 Wikipedia 中定义的 Z 组合子不同。根据 Wikipedia,Z 组合器和“按值调用 Y 组合器”是同一个东西,看起来这确实是更常被称为“应用顺序 Y 组合器”的东西。

所以,我上面的内容与通常编写的应用顺序 Y 组合子相同,但几乎可以肯定它们之间存在某种关联。以下是我进行比较的方法:

开始于:

Y_letrec = λf . (λx.x x) (λs . (λa . (f ((λx.x x) s)) a))

应用内 U:

Y_letrec = λf . (λx.x x) (λs . (λa . (f (s s)) a))

应用外U:

Y_letrec = λf . (λs . (λa . (f (s s)) a)) (λs . (λa . (f (s s)) a))

重命名以匹配 Wikipedia 对 Z 组合子的定义:

Y_letrec = λf . (λx . (λv . (f (x x)) v)) (λx . (λv . (f (x x)) v))

将此与维基百科的 Z 组合器进行比较:

Z        = λf . (λx . f (λv . ((x x) v))) (λx . f (λv . ((x x) v)))

显着的区别在于函数f 的应用位置。有关系吗?尽管存在这种差异,这两个功能是否等效?

【问题讨论】:

  • 我记得,应用顺序与正常顺序是相反的。在应用顺序中,诸如 Scheme 参数之类的语言会在函数看到它们之前立即进行评估。这使得定义 Y 组合器变得复杂。在正常的顺序中,就像在传统的 lambda 演算中一样,参数被传递并且仅在没有其他选项时才被评估。 Y 组合器按正常顺序更简单,例如Hindley and Seldin 页。 34.

标签: scheme lisp combinators y-combinator anonymous-recursion


【解决方案1】:

是的,它是一个应用顺序 Y 组合子。在里面使用 U 完全没问题,我也这样做了(参见fixed point combinator in lisp)。使用 U 来缩短代码是否有名称,我不这么认为。这只是一个 lambda 项的应用,是的,它也让 IMO 更清晰。

确实有一个名字,就是 eta-conversion,在你的代码中用于延迟应用顺序下的评估,其中参数的值必须在函数应用之前知道。

在你的代码((λa.(f (s s)) a) ==> f (s s))上应用 U 并执行 eta-reduction 后,它变成了熟悉的正序 Y 组合子 - 即在正序评估下工作,其中在函数应用之前不需要参数的值,这可能最终不需要它们(或其中一些):

Y = λf . (λs.f (s s)) (λs.f (s s))

顺便说一句,延迟可以以稍微不同的方式应用,

Y_ = λf . (λx.x x) (λs.f (λa.(s s) a)) 

这也适用于应用顺序评估规则。

有什么区别?让我们比较一下归约序列。你的版本,

Y_ = λf . (λx . (λv . (f (x x)) v)) (λx . (λv . (f (x x)) v))

((Y_ f) a) = 
  = ((λx . (λv . (f (x x)) v)) (λx . (λv . (f (x x)) v))) a
  = (λv . (f (x x)) v) a    { x := (λx . (λv . (f (x x)) v)) }
  = (f (x x)) a
  = | ; here (f (x x)) application must be evaluated, so 
    | ; the value of (x x) is first determined
    | (x x) 
    | = ((λx . (λv . (f (x x)) v)) (λx . (λv . (f (x x)) v))) 
    | = (λv . (f (x x)) v)     { x := (λx . (λv . (f (x x)) v)) }

这里输入了f。所以在这里,行为良好的函数f 接收它的第一个参数,它不应该对它做任何事情。所以也许这两者毕竟是完全等价的。

但实际上,当涉及到真正的实现时,lambda 表达式定义的细节并不重要,因为真正的实现语言会有指针,我们只需操纵它们正确指向包含的表达式主体,而不是它的副本。 Lambda 演算毕竟是用铅笔和纸完成的,作为文本的复制和替换。 lambda 演算中的 Y 组合子仅模拟递归。真正的递归是真正的自引用;不接收副本等于自我,通过自我应用(无论多么聪明)。

TL;DR:虽然被定义的语言可能没有赋值和指针相等等有趣的东西,但我们定义它的语言肯定会有这些,因为我们需要它们来提高效率。至少,它的实现将在底层拥有它们。

另见:fixed point combinator in lisp,尤其是。 In Scheme, how do you use lambda to create a recursive function?.

【讨论】:

  • 非常感谢。有理由偏爱一种形式而不是另一种形式吗?经过大量的探索和修补,我最终找到了我的,当我发现它并不完全相同时,我感到很惊讶。
  • 缩减顺序有一点不同。我认为您的版本实际上更好,它确保减少停止;如果f 的行为符合预期,通常会更进一步并停止。
  • 历史上第二版的增补有相关的推导。 (虽然,关于 U 作为延迟设备仍然存在错误——它不是;只有 eta-expansion 是)。我可以在这里挖掘出来并添加到答案中。
  • 啊,我知道我的意思 - 在计算 (Y_ f) 时,(not ((Y_ f) a)),您的版本会提前一个减少步骤停止,它会自行停止,无需进入f。标准 def 最终将应用 (λs.f (λa.(s s) a)) (λs.f (λa.(s s) a)) == f (λa.(s s) a) { s := (λs.f (λa.(s s) a)) } 并进入 f 定义。所以标准的 def 太急切了;你的很懒惰。恭喜!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-08
  • 1970-01-01
  • 2013-06-03
  • 1970-01-01
  • 2010-12-05
  • 1970-01-01
相关资源
最近更新 更多