当前位置: 首页 > 科技观察

10种编程语言实现Y组合子

时间:2023-03-17 22:46:25 科技观察

—Y-CombinatorY组合子是Lambda演算的一部分,是函数式编程的理论基础。这是一种在没有赋值语句的情况下定义递归匿名函数的方法/技巧。即只通过Lambda表达式最基本的“原子”实现循环/迭代,有道生一,一生二,二生三,三生万物的感觉。1从递归阶乘函数开始,写出最简单的递归阶乘函数,不考虑效率等其他因素。这里使用Scheme,大家可以选择自己熟悉的编程语言,跟着我一步步实现Y-Combinator版的阶乘函数。(define(factorialn)(if(zero?n)1(*n(factorial(-n1)))))(define(fn-name))在Scheme中是(definefn-name(lambda))的简写,只是与在JS中一样,functionfoo(){}等同于varfoo=function(){}。把上面的定义展开成Lambda定义:(definefactorial(lambda(n)(if(zero?n)1(*n(factorial(-n1))))))2绑定的函数名要调用递归函数,你必须给这个函数一个名字。如果一个匿名函数想要实现递归,它就不得不取一个临时的名字。所谓临时的,就是这个名字只在函数体内有效。函数执行后,名称随函数一起消失。为了解决这个问题,第一篇文章[1]中强制要求匿名函数有一个指向自身的隐藏名this,导致变量名this被强行占用,不优雅,所以第二篇文章[2]借鉴了允许自定义名称的Clojure方法。但是在Lambda演算中,只有最常见的Lambda,没有赋值语句,怎么绑定一个名字呢?答案是使用Lambda的参数列表!(lambda(factorial)(lambda(n)(if(zero?n)1(*n(factorial(-n1))))))3虽然生成阶乘函数的函数使用了参数列表,即使用闭包技术给匿名函数起个名字,这个函数并不是我们想要的阶乘函数,而是阶乘函数的元阶乘,也就是生成阶乘函数的函数。因此,您需要执行此元函数以获得所需的阶乘函数:((lambda(factorial)(lambda(n)(if(zero?n)1(*n(factorial(-n1)))))xxx)这就又产生了一个问题:实参xxx,即形参factorial应该取什么值呢?从定义上看,factorial就是函数本身。既然是“self”,首先想到的就是复制完全相同的代码:((lambda(factorial)(lambda(n)(if(zero?n)1(*n(factorial(-n1)))))(lambda(factorial)(lambda(n)(if(zero?n)1(*n(factorial(-n1)))))))好像是把自己传给自己了,但是立马发现(factorial(-n1))会失败,因为factorial不是a此时的阶乘函数,而是一个包含阶乘函数的函数,即获取里面包含的函数,所以调用方式要改为((meta-factorialmeta-factorial)(-n1)):((lambda(meta-factorial)(lambda(n)(if(zero?n)1(*n((meta-factorialmeta-factorial)(-n1))))))(lambda(meta-factorial)(lambda(n)(if(zero?n)1(*n((meta-factorialmeta-factorial)(-n1)))))))将名称更改为meta-factorial以清楚地看到它是阶乘的元函数,而不是阶乘函数本身。4去除重复上面的代码已经实现了lambda的自调用,但是其中包含了重复的代码,meta-factorial既是一个函数又是一个参数,即(metameta):((lambda(meta)(metameta))(lambda(meta-factorial)(lambda(n)(if(zero?n)1(*n((meta-factorialmeta-factorial)(-n1)))))))5提取阶乘函数,因为我们想要阶乘函数,因此将(meta-factorialmeta-factorial)替换为factorial,该方法也使用参数列表命名:((lambda(meta)(metameta))(lambda(meta-factorial)((lambda(factorial)(lambda(n)(if(zero?n)1(*n(factorial(-n1)))))(meta-factorialmeta-factorial))))这段代码无法正常运行,因为Scheme等主流编程语言实现使用“应用顺序”,即函数执行时先计算参数的值,所以(meta-factorialmeta-factorial)本来是在计算阶乘的过程中执行的,现在执行时间为先进的抽取之后,就陷入了死循环。解决方案是将其包装在Lambda中(您了解了Lambda的另一个用途:延迟执行)。((lambda(meta)(metameta))(lambda(meta-factorial)((lambda(factorial)(lambda(n)(if(zero?n)1(*n(factorial(-n1))))))(lambdaargs(apply(meta-factorialmeta-factorial)args)))))此时,代码中的第4行到第8行正是最初定义的匿名递归阶乘函数,我们终于有了阶乘函数本身!6形成模式如果将阶乘函数作为一个整体抽取出来,就是一个可以生成任意匿名递归函数的“模式”:((lambda(fn)((lambda(meta)(metameta))(lambda(meta-fn)(fn(lambdaargs(apply(meta-fnmeta-fn)args))))(lambda(factorial)(lambda(n)(if(zero?n)1(*n(factorial(-n1)))))))在Lambda演算中,这种模式被称为Y-Combinator(Y-Combinator),即:(define(y-combinatorfn)((lambda(meta)(metameta))(lambda(meta-fn)(fn(lambdaargs(apply(meta-fnmeta-fn)args))))))有了Y组合器,我们可以定义任意匿名递归函数,前面的定义是递归阶乘,然后定义一个递归斐波那契数:(y-combinator(lambda(fib)(lambda(n)(if(36288002Clojure实际上,Clojure可以在不使用Y-Combinator的情况下实现匿名递归函数,其lambda-fn-支持传递一个函数名来命名这个临时函数。也许Clojure的fn不应该叫匿名函数,应该叫临时函数更合适。同样是Lisp,Clojure版本比Scheme版本短,但是它使得我觉得是刻意的简洁,我喜欢用fn而不是lambda,但是用奇怪的符号来减少代码量会降低代码的可读性(我最近好像不太喜欢符号了,哈哈)。(defny-combinator[f](#(%%)(fn[x](f#(apply(xx)%&))))((y-combinator(fn[factorial]#(if(zero?%)1(*%(factorial(dec%)))))10)3CommonLispCommonLisp版本其实和Scheme版本差不多,只不过CommonLisp属于Lisp-2,即函数命名空间不同于变量命名空间,所以调用匿名函数需要额外的funcall。个人不喜欢这个额外的调用,我觉得是多余的信息,职位信息已经包含了角色信息,就像命令行的第一个参数永远是命令一样。(defuny-combinator(f)((lambda(u)(funcalluu))(lambda(x)(funcallf(lambda(&restargs)(apply(funcallxx)args))))(funcall(y-combinator(lambda(阶乘)(lambda(n)(if(zeropn)1(*n(funcallfactorial(1-n))))))10)4RubyRuby从Lisp中借鉴了很多,包括它的缺点。像CommonLisp,在Ruby也需要额外的“.call”,或者用方括号“[]”代替普通函数一样的圆括号“()”。总之,Ruby中的匿名函数和普通函数一样,功能不同!有还有一些复杂的符号,也影响了我在Ruby中使用匿名函数的心情,所以我会把Ruby看成语法更灵活简洁的Java,不会考虑写函数式风格的代码。defy_combinator(&f)lambda{|&u|u[&u]}.calldo|&x|f[&lambda{|*a|x[&x][*a]}]endendy_combinatordo|&factorial|lambda{|n|n.zero??1:n*factorial[n-1]}end[10]5Python匿名函数在Python中的使用方法和普通funct一样离子。就这段代码而言,Python之于Ruby就像Scheme之于CommonLisp。但是Python对Lambda的支持简直弱到不行,函数体内只能有一条语句!我决定在我的工具箱中用Python替换C,尽管Python对匿名函数的支持只比C好一点点。defy_combinator(f):返回(lambdau:u(u))(lambdax:f(lambda*args:x(x)(*args)))y_combinator(lambdafactorial:lambdan:1ifn<2elsen*factorial(n-1))(10)6Perl我个人对无法在Perl函数中声明参数的抱怨不仅仅是复杂的符号!suby_combinator{my$f=shift;sub{$_[0]->($_[0]);}->(sub{my$x=shift;$f->(sub{$x->($x)->(@_);});});}printy_combinator(sub{my$factorial=shift;sub{$_[0]<2?1:$_[0]*$factorial->($_[0]-1);};})->(10);假设Perl可以像其他语言一样声明参数列表,代码会更简洁直观:suby_combinator($f){sub($u){$u->($u);}->(sub($x){$f->(sub{$x->($x)->(@_);});});}printy_combinator(sub($factorial){sub($n){$n<2?1:$n*$阶乘->($n-1);};})->(10);7JavaScriptJavaScript无疑是最流行的脚本语言!但是冗长的function、return等关键字总是刺痛我的神经:).apply(null,arguments);});});};y_combinator(function(factorial){returnfunction(n){return<=1?1:n*factorial(n-1);};})(10);ES6提供了=>语法,可以更简洁:consty_combinator=fn=>(u=>u(u))(x=>fn((...args)=>x(x)(...args)));y_combinator(阶乘=>n=>n<=1?1:n*阶乘(n-1))(10);8LuaLua和JavaScript有同样的问题。最让我吃惊的是它没有三元运算符!但是不使用大括号的代码看起来更简洁~functiony_combinator(f)return(function(u)returnu(u)end)(function(x)returnf(function(...)returnx(x)(...)end)end)endprint(y_combinator(function(factorial)returnfunction(n)returnn<2and1orn*factorial(n-1)endend)(10))注意:Lua5.25.1版本的语法不同,需要将x(x)(...)与x(x)(unpack(arg))。9PHPPHP也是JavaScript的难兄难弟,function,return……另外,PHP版使用脚本语言中最多的符号($、_、()、{})!是的,超过Perl。{Call(Object...args);}publicstaticLambdaandCombinator(finalLambdaf){returnnewLambda(){@OverridepublicLambdacall(Object.args){finalLambdau=(Lambda)args[0];返回。调用(u);}}。call(newLambda(){@OverridepublicLambdacall(Object...args){finalLambdax=(Lambda)args[0];returnf.call(newLambda(){@OverridepublicObjectcall(Object...args){returnx.call(x).call(args)}});}});}publicstaticvoidmain(String[]args){Lambday=yCombinator(newLambda(){@OverridepublicLambdacall(Object...args){finalLambdafactorial=(Lambda)args[0];returnnewLambda(){@OverridepublicIntegercall(Object...args){Integern=Integer.parseInt(args[0].toString());if(n<2){returnInteger.valueOf(1);}else{returnn*factorial.call(n-1);}}};}});;System.out.println(y.call(10));}}相关链接[1]http://zzp.me/2011-08-05/recursive-lambda/[2]http://zzp.me/2012-08-04/clojure-style-lambda-in-common-lisp/