老虎必须追捕,鸟飞得苍蝇; Lisper得坐下来,(Y(Y Y))?老虎睡了,鸟来落地; Lisper得告诉自己他了解。 - Kurt Vonnegut,由Darius Bacon修改
我最近写了一个关于y组合者的博客文章。从那以后,我收到了许多有用的评论,我认为将帖子扩展到更完整的文章中是合适的。本文将对主题进行更深入的深度,但我希望它能够更加可理解。你不需要阅读前一篇文章以了解这一点(其实,如果你没有,那么它可能更好,如果你verven')我所需要的唯一背景知识是对该方案的微小了解编程语言包括递归和一流的功能,我将审查。评论(再次)欢迎。
在我进入y实际上的细节之前,我想解决为什么你作为程序员的问题应该烦恼地了解它。诚实的,那里an'塔很多好螺母 - 螺栓的实际原因,即使它确实存在了一些实际应用,大部分都是计算机语言理论家的兴趣。无论如何,我觉得它'值得你的虽然是关于你的想法:
它'在所有编程中最美丽的想法之一。如果您有任何编程美学的感觉,则' Quecate将被Y愉快地高兴。
它以一种非常显着的方式显示了功能性编程的简单思想。
1959年,英国科学家C. P. Snow给了一个着名的讲座,称为两种文化,他讨论了许多智能和受过良好的人们的时间几乎没有科学知识。他使用了对那些科学识字的人之间的第二种热力学定律,以及那些温和的人之间的分界线和' t。我认为我们可以类似地使用Y组合者的知识作为&#34的程序员之间的分界线;功能识字" (即对功能规划的合理知识)和那些aren的人和#39; t。还有其他议题可以服务,也可以(特别是MONADS),但y会很好。所以,如果你渴望拥有真正的兰布 - 性质,请阅读。
顺便说一下,Paul Graham(Lisp黑客,Lisp Book Authory,Essayist和现在风险资本主义)显然非常重要,他将他命名为他的启动孵化器公司Y Combinator。保罗从他的想法知识中富有丰富;也许其他人也会。也许甚至你。
我们' ll通过定义一些函数来计算阶段来开始探索Y组合器。非负整数N的源极是从1开始的所有整数的乘积,并延长到和包括n。因此我们有:
因子1 = 1因子2 = 2 * 1 = 2因子3 = 3 * 2 * 1 = 6因子4 = 4 * 3 * 2 * 1 = 24
等等。 (i' m使用没有括号的函数表示法,所以armential 3与通常写入的因子(3)。幽默我。)因子越来越多地增加; 20的因子为2432902008176640000。0的因子是1;事实证明,对于实际用于造票的种类的适当定义(如解决问题,议题蛋白)。
它易于在编程语言中编写功能,以使用某种循环控制构造的编程语言中的函数,例如循环或循环(例如,在C或Java中)。但是,它也很容易编写递归函数来计算阶乘,因为阶乘有一个非常自然的递归定义:
第二行适用于大于零的所有n。 Infact,在计算机语言Haskell,那就是您实际定义因子函数的方式。在方案中,我们&#39的语言; ll在这里使用,这个函数将写类似:
方案使用括号前缀符号为所有内容,因此类似于( - n 1)表示通常在大多数编程语言中写入的n - 1。这的原因超出了本文的范围,但习惯了这个符号,非常努力。
事实上,上述方案中的阶乘函数的定义也可以以稍微明确的方式编写如下:
(定义因子(lambda(n)(如果(= n 0)1(* n(armborial( - n 1)))))))))
关键字Lambda简单地表明了我们&#39的东西;重新定义(即,由打开的括号封闭到Lambda的左侧和其相应的衣服内部)是一个功能。在Lambda中,在括号中的话是什么之后,是Function的正式论点;这里只有一个参数,这是n。该函数的主体在正式参数之后,以及表达式的秘书主义者(如果(= n 0)1(* n(armborial( - n1))))))。这种功能是匿名功能。在这里,你给了你匿名函数你&#39之后的姓名因子; vedefined它,但你不必,并且经常它' s handy不是如果你'重新使用它一次。在方案和一些其他语言中,匿名功能也称为Lambda表达式。除了方案之外的许多编程规程允许您定义匿名函数,包括PyThon,Ruby,JavaScript,OCAML和Haskell(但不遗憾的是C,C ++或Java)。我们' ll在下面使用lambda表达式。
在方案语言中,因子的定义仅仅与之相同;方案在评估之前简单地将FirstDefinition转换为第二个。所以所有功能术语都是真正的lambda表达。
请注意,该函数的主体对其中的FactorialFunction('在定义过程中的过程中的调用,这使得该递归定义。我会调用这种定义,其中定义的函数的名称用于函数的主体,一个明确的递归定义。 (你可能想知道一个"隐式的副顾忌"功能是。我不会使用那种表达,但我想到的是一个递归函数,它是通过未经递归手段生成的递归函数 - 继续阅读!)
为了参与争论,我们将假设我们的Schemedoesn和#39; t的版本有相当于循环Inc或Java(但实际上,实际方案实现确实有这样的结构,但在不同的情况下名称),以便为了定义函数函数,我们几乎必须使用递归。计划以外用作教学语言的原因:它迫使学生学习者学会递归思考。
方案是一种很多原因的酷语言,但是一个与之相关的语言是它允许您使用"头等舱"数据对象(通常通过说方案支持第一类功能)来表示。这意味着在方案中,我们可以将函数传递给另一个功能作为参数,我们可以返回一个函数,因为应用于其参数的评估another函数的结果,我们可以在我们需要时创建CombourcyOn-Fly(使用lambda符号表示)。这是功能规划的本质,它将在随后的讨论中括起来。使用其他功能的函数和/或将其他函数返回其结果,通常是更高阶函数的。
现在,这里'拼图:如果要求您在方案中定义阶乘函数,则何时才能告知您可以在定义中通知递归函数调用(例如,在上面给出的因子函数中,您无法使用词因子在函数的身体中的任何地方)。但是,无论如何,允许您使用一流的功能和高阶函数,您认为您会看到合适。通过这种知识,您能定义因子函数吗?
这个问题的答案是肯定的,它将直接向我们带到Ycombinator。
Y组合器是一个高阶函数。它需要一个参数,这是一个函数,即递归。它返回递归的功能版本。我们将在下面使用y详细介绍从非递归人生成递归功能的过程,但是基本想法。
更一般地说,Y为我们提供了一种方法来在编程语言中获得递归,支持一流的功能,但它没有内置递归。所以你们表明我们的是这种语言已经允许我们允许我们过期函数,即使语言定义本身表示缺点递归。这是一件美丽的事情:它向我们展示了单独的功能性能可以让我们做我们永远不会期望Beable的事情(而且它不是这个问题的唯一例子)。
我们将看一下两种广泛的计算机语言:那些使用懒惰评估的人和使用严格评估的人。 lazyevaluation意味着为了评估语言中的表达,您可以评估最终结果所需的表达式。等(例如)如果有一部分表达式,则需要忘记评估(因为结果不依赖于它)它赢得了' t beevalated。相反,严格的评估意味着在确定ASA整体的表达式的价值之前,将在表达式的值(具有一些必要的例外)之前完全评估所有部分,例如表达式,这必须懒得正常工作)。懒惰评估更为一般,但严格的评估是更为预测和往往更有效的。大多数编程语言使用严格的评估。编程语言Haskell使用懒惰的评估,这是关于该语言最有趣的事情。我们将在下面使用两种类型的评估。
尽管我们经常指Y和#34;" y组合器,实际上是一个无限数量的y组合器。我们只会关注两个人,一个懒惰和一个严格的。我们需要两个y组合器,因为Ycombinator我们为懒人语言定义不适用于严格的语言。懒惰的Y组合器通常被称为正常阶Ycombinator,严格的组合器被称为适用于Applicated-Order Ycombinator。基本上,正常顺序是另一种说法的方式"懒惰"和申请订单是另一种说法的方式"严格"
在编程语言中的另一个大划分线是统计学和动态键入之间。静态类型的语言是在编译时确定所有表达式的类型,并且任何类型的错误都会导致编译失败。动态类型的语言不会' t doany型检查直到运行时间,并且如果函数应用于某种类型的参数(例如,通过尝试将整数和字符串添加在一起),则报告错误。在常用的编程语言中,C,C ++和Java是静态键入的,而Perl,Python和Ruby是动态的。方案(我们的语言我们和我们的例子使用的语言)是独奏动力学的。 (也有语言跨越边界之间的典型和动态类型,但我赢得了' T讨论了这件事。)
人们经常听到静态打字,称为强键入的曲折打字,这是一种弱打字,但这是滥用的。强的打字简单意味着语言中的每个值都有一个类型,而只有一种类型,而弱打字意味着某些值可以获得任何类型的类型。因此,动态类型的方案也被强烈打字,而C静态键入的C是弱键入的(因为您可以将一个物体投射到指向另一类型对象的指针中的指针' s价值)。我将只关注这里的强烈打字语言。
事实证明,以动态的语言定义y组合器更简单,所以' s我' ll做。可以以许多静态类型的语言定义Ycombinator,但(至少在示例中和#39; ve看到)这些定义通常需要一些非明显的类型的黑掩饰,因为Y组合器本身并不是直截了当的静态类型。除了本文的范围之外,我赢得了'我进一步提到它。
组合者只是一个λ表达式,没有自由偏离。我们看到了λ表达式(它们' justanonymous函数),但是一个免费变量'它' s一个变量(即语言中的名称或标识符),哪个是姓名的。现在开心?不?好的,让我解释一下。
绑定变量简直是一个变量,该变量包含在Lambda表达式的主体内,该λ表达式具有该变量名称作为其参数之一。
这些lambda表达式的体变量是免费变量还是绑定变量?我们' ll忽略了lambda表达式的形式参数,因为λ表达的身体中只能被视为自由或绑定的变量。至于其他变量,这里有答案:
λ表达体内的X是一个边界,因为λ表达的正式参数也是x。这种λ表达没有其他变量,因此没有自由变量,因此' s组合器。
Lambda身体中的Y是一个自由变量。因此,这种λ表达不是组合者。
除了Lambda表达的形式参数之外,只有一个变量,最终x,这是一个绑定变量(由外部λ表达的正式参数绑定)。因此,这个Lambda表达式整体没有自由变量,所以这是一个组合者。
除了Lambda表达式的形式参数之外,还有两个变量,最终x和y,两个绑定变量。这是一个组合者。
整个表达不是λ表达,所以它根据定义而不是组合者。然而,X是一个自由变量,最终y是绑定变量。
再次,整个表达是' t一个λ表达(它' s函数应用程序),所以这是一个组合者。第二x是绑定变量,而Y是自由变量。
(定义因子(lambda(n)(如果(= n 0)1(* n(armborial( - n 1)))))))))
是一个组合者,你不考虑定义部分,所以你'重新询问是什么
是一个组合者。由于在该Lambda表达式中,名称阶乘代表一个自由变量(名称阶乘不是Lambda表达式的正式参数),这不是组合者。这将是重要的。事实上,名称=,*,和 - 也是免费变量,所以即使没有名称因子,这不是一个组合者(说不出数字!)。
(定义因子(lambda(n)(如果(= n 0)1(* n(armborial( - n 1)))))))))
我们想要做的就是提出这一版本,这是同样的事情,但并不是' t在函数体内有讨厌的递归调用阶乘呼叫。
我们在哪里开始?如果您可以保存所有功能,除了违规递归调用之外,它会很好,并在那里放一些东西。这可能是这样的:
(定义局部排序(如果(= n 0)1(* n(&lt ;??>( - n 1))))))))))
这仍然让我们留下了标有标记的地方的问题。它'如果您不知道您想要在一段代码中的某个地方究竟知道您想要放入某个代码的某个地方,请将其摘录并使其成为函数的参数,这是一个验证和真正的功能编程原则最简单的方法如下:
(定义几乎 - 因子(Lambda(F)(如果(= n 0)1(* n(f( - n 1)))))))))
我们在这里完成了什么是将递归调用重命名为F的递归调用,并使F Anargument与我们'重新调用近极某的函数。差异差异不是所有的因子函数。相反,它'一个高阶函数,它采用一个参数f,它更好地是一个函数(或f( - n1))won' t有意义),并返回另一个函数((如果我们为F选择合适的价值,那么(希望)将是一个(希望)的部分)。
它很重要,意识到这个技巧不是以任何方式特定于阶乘函数。我们可以用任何递归函数做出完全相同的技巧。例如,考虑递归函数来计算FibonAcci数字。 Fibonacci号码的递归定义如下:
fibonacci 0 = 0 fibonacci 1 = 1 fibonacci n = fibonacci(n - 1)+ fibonacci(n - 2)
(事实上,'在Haskell中的Fibonacci函数的定义。)在方案中,我们可以以这种方式编写功能:
(定义Fibonacci(lambda(n)(cond(= n 0)0)((= n 1)1)(else(+(fibonacci( - n 1))(fibonacci( - n 2)))))))))) )
(COND只是嵌套如果表达式的速记表达式)。然后,我们可以像我们为因素做的那样删除显式递归:
(定义几乎 - 斐波纳(Lambda(F)(Lambda(n)(cond((= n 0)0)((= n 1)1)(否则(+(f( - n 1))(f( - n 2))))))))))
正如您所见,从递归函数到非递归几乎等效函数的转换是纯机械唯一的:您将函数函数的名称重命名为f到f,并且您包装a(lambda(f )......)在身体周围。
如果你'我跟着我刚刚做了什么(从不介意为什么我做到了;我们稍后会看到那个),然后祝贺!正如尤达所说,你刚刚走进了一个更大的世界。
我可能不应该这样做,但是我会给你一个偷偷摸的预览我们'重新进入的地方。一旦我们定义了Y组合器,我们就可以使用几个差异定义差异函数,如下所示:
其中y是y组合器。请注意,此因子的定义不具有任何明确的递归。同样,我们可以以相同的方式使用近似 - 斐波纳契定义Fibonacci函数:
因此,只要我们拥有适当的几乎函数(即,通过抽出递归函数调用,我们将在何时何时何时何地提供额外的次数
为了论述,假设我们已经拥有了一个工作阶乘函数(递归或不,我们不小心)。我们' ll致电该假设的阶乘函数因子。现在让' s考虑以下内容:
(定义FactoriaNB((λ(λ(n)(如果(= n 0)1(* n(f( - n 1))))))))
现在,通过在我们得到的Lambda表达式身体内代替f的facoriala:
(定义FactorizerB(lambda(n)(如果(= n 0)1(* n(factoriala( - n 1)))))))))
这看起来很像递归阶段函数,但它是' t:factoriala与factorizerb.so' s一个非递归函数,这取决于假设的事项a函数。它实际上有效吗?嗯,它' Spretty显然,它应该为n = 0工作,因为(FactorizerB 0)将只是返回1(所0的因子)。如果是N> 0,然后(FactorialBn)的值将是(* n(factoriala( - n 1))))。现在,我们假设事项是正确计算阶乘的影响,因此(因此( - n 1))是n - 1的因子,因此(* n(factoriala( - n 1)))是n的因子(通过定义阶乘),从而证明,只要各种情况,就可以正确计算因子函数。所以这是有效的。唯一的问题是我们不'智能地有一个撒谎的局势。
现在,如果你真的很聪明,你可能会问自己是否可以这样做:
这一想法是:假设Sfaciala是一个有效的因子函数。然后
......