您的位置:首页 > Web前端 > JavaScript

用Python的函数式编程特性解释Y Combinator(仿JavaScript版)

2017-07-20 16:40 375 查看
函数式编程(Functional Programming)曾经火过一阵,但是一直没有在工业领域流行起来,其实函数式编程思想早就融入通用语言,常见的JavaScript,Python,Ruby都支持函数式编程特性,甚至C++11标准也引入了匿名函数。过去曾在这个博客 http://blog.csdn.net/g9yuayon 看到过介绍【lambda算子】的一系列文章,当时有种大开眼界的感觉,原来计算能以这种方式表达,可惜没有进一步学习FP的零零总总。前阵子又再次瞥见其中一篇文章用JavaScript模拟Y组合子(http://blog.csdn.net/g9yuayon/article/details/1271319),心想着既然最近的工作都在用Python,干脆依葫芦画瓢用Py也来实现一遍吧。

Y组合子是为了解决递归调用的问题,在FP中递归是一个很重要的概念,尽管具名函数调用自己是一件普通的事,但是匿名函数要怎样调用自己呢?匿名函数在FP中也是不可缺的。那为什么要多此一举呢,直接给个名字不好吗?FP的理论基础在数学上是严谨的,追求简洁精致又要完备,如果不用函数名就能实现递归,那么就别用,于是Y组合子就应运而生了。下面要做的就是用Python的语法特性解释这个过程。

有必要明白什么是不动点。不动点是函数的一个特征:对于函数f(x),如果有变量a使得f(a)=a成立,则称a是函数f上的一个不动点。我们想要找到一个这样的函数Y,使得当传入匿名的递归函数时得到的还是一个同样的匿名函数。

首先定义一个计算阶乘的递归函数,so easy...

def fact(n):
if n == 0:
return 1;
if n > 0:
return n * fact(n - 1)

print fact(5)

fact是个基本的函数,调用了它自己,但是如果不允许它内部出现fact这个名字怎么办,可以假设存在一个计算阶乘的函数h,将它当作参数传入。

fact = lambda h, n: 1 if n < 2 else n * h(h, n-1)

这时就定义了一个匿名函数,为了使用方便先将它赋值给fact,调用fact的时候可以将fact自己传递给自己当参数,毕竟函数是FP中的一等公民嘛,像这样:

print fact(fact, 5)

可是经典的lambda算子按理说只接受一个参数的,虽然各种编程语言允许接受多个参数,但还是需要回归本质嘛,于是变一下,在匿名函数里面返回一个匿名函数,如下:

fact = lambda h: lambda n: 1 if n < 2 else n * h(h)(n-1)
print fact(fact)(5)

可见调用方法也发生了一些变化,一路做的事情有点类似重构,到目前这个函数还可以继续重构。最终想要得到的东西是Y组合子的实现,即解决自我引用的问题,这里用阶乘函数来作为递归的范例,换成其他递归算法也要被支持,所以接下来应该分离出这个阶乘函数,就像实际工作中程序框架和业务逻辑肯定也不应该耦合。我们观察到这一部分:lambda n: 1 if n < 2 else n * h(h)(n-1) 形式上和阶乘递归函数很像,多出来的是 h(h) 这个函数调用,不如将其参数化:

fac_gen = lambda q: lambda n: 1 if n < 2 else n * q(n-1)

显而易见,这里的参数q其实就是上面的 h(h),虽然目前并不知道它们具体是什么东西,但是 fac_gen(h(h))(n) 的结果为
1 if n < 2 else n * h(h)(n-1) 是显而易见的,正好可以替换一下上面那个fact,于是得到:

fact = lambda h: lambda n: fac_gen(h(h))(n)
print fact(fact)(5)

具体的阶乘部分其实相当于业务逻辑,也可以换做其他的递归函数比如计算斐波那契数列,可以视具体业务fac_gen为一个参数f,进而得到以下的函数:

gen = lambda f: lambda h: lambda n: f(h(h))(n)

显然,当fac_gen通过参数f传入,上面的fact就等价于gen(fac_gen),原来的调用语句 fact(fact)(5) 可以用 gen(fac_gen)(gen(fac_gen))(5) 替换了,不妨print出来验证一下结果。这样,我们有了gen这个函数,它可以对一个不能直接调用的递归函数作转换从,而得到可以实际调用的函数。只是这样的调用写法太累赘了,我们最终需要的是Y,使得Y(fac_gen)等价于g(fac_gen)(g(fac_gen)),干脆再整理一下,让目标更清晰:

fact == gen(fac_gen) == lambda h: lambda n: fac_gen(h(h))(n)
Y(fac_gen) == fact(fact) == gen(fac_gen)(gen(fac_gen)) == (lambda g: g(g))(gen(fac_gen))

这下看到曙光了,再次把fac_gen提取成参数f,我们得到了Y的定义:

Y = lambda f: (lambda g: g(g))(lambda h: lambda n: f(h(h))(n))
print Y(fac_gen)(5)

革命终于成功了!还可以进一步完善,毕竟fac_gen的参数只需要一个n,如果是其他函数可能需要传入不止一个参数,因此我们可以利用python的语法特性,以*args来代替n,最终形式:

Y = lambda f: (lambda g: g(g))(lambda h: lambda *args: f(h(h))(*args))

除了fac_gen,还可以写个其他的递归函数来验证一下,这里用到了计算最大公约数的递归函数,如下:

gcd_gen = lambda q: lambda n, m: m if n%m == 0 else q(m, n%m)
print Y(gcd_gen)(8, 6), Y(gcd_gen)(18, 81)

Y组合子这玩意真是神奇,虽然推导的过程不需要什么高深的知识,但还是难以想象什么样的大脑会第一个把它描述出来。说实话,如果现在不给我任何参考,让我用python一步步推导出Y Combinator的最终形式,恐怕还不一定能顺利搞定。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Y组合子 lambda演算