构造Y组合子
2016.12.07 17:12:37
Y组合子(Y-Combinator)是Lambda演算的一部分,也是FP编程中为人所津津乐道的一种方法。对于FP程序员来讲,估计也仍有不少人对其要么陌生、要么茫茫然。
Y组合子能够神奇地利用匿名函数/Lambda的方式来表述递归调用。Y组合子或不动点组合子的概念可以参考各类百科,这里就不再赘述。
由于最近在钻研Go语言,因此这里就用Go语言进行描述(文章最后也会列出其他几种语言的Y组合子实现)。但Y组合子的概念实现思路都是一致的,掌握了思路,用其他语言实现应该没问题。
话说回来,由于Go语言的类型语言特点,在Go中实现Y组合子很容易被函数定义绕晕了,动态语言则实现起来相对轻松一些。
接下来一步步解释Y组合子构建思路。
在这里,我们随意选择了「The Go Programming Language」中的一个例子,作为我们的讲解案例:
func comma(s string) string { n := len(s) if n <= 3 { return s } return comma(s[:n-3]) + "," + s[n-3:] }
该示例用来对 1234567890
这样的字符串进行自右往左每隔3个字符加一个逗号: 1,234,567,890
。
这是构建递归调用的传统思路,一般情况下,我们的递归就是像上述代码这样实现的。可以称之为一般性递归(Natural Recursion)。
我们可以发现,在一般性递归中,必须有个函数名,才能使用递归调用。而Y组合子的神奇之处就在于,它能够利用匿名函数/Lambda的方式来表述递归调用。
这是如何做到的?要消除函数名,关键是构建一个能够“自己调用自己”的匿名函数——这是很自然就能想到的,否则无法消除函数名。
下面,看看我们是怎么玩的。
纵使是匿名函数,我们姑且也给这个“无名”一个名字—— f
,因此 f
就变成了(伪代码):
f{ return f(s[:n-3]) + ... }
接下来,我们就需要确实消除这个“无名”的名,这可听起来像是什么高深莫测、难以理解的高深武功呐。别急,一步步来。
首先,我们需要添加一个真正的匿名函数作为包裹函数(伪代码):
func(f) { return ... }
这里的 return
该返回啥呢?我们知道 f
是个递归,如果只返回 f
是不够的,那跟直接使用 comma
没什么区别,反而造成内层的 f(s[:n-3])
递归调用无法实现,因为——“无名”。
要返回一个匿名形式的递归函数,也就是要返回一个不断调用自身的函数,其形式为:
f(f)
这个很关键。所以有了这样的代码(伪代码):
func(f) { return f(f) }
上述伪代码就实现了匿名函数递归调用自身的功能,外层加了个包裹函数是为了返回这个递归调用的匿名函数 f(f)
。
不管怎么变来变去的,我们要的是 comma
的最初实现。包装器的返回类型必然跟 comma
函数的类型定义一样:
func(string) string
所以,我们定义一个 baseFnType
,其跟 comma
的类型定义一样:
type baseFnType func(string) string
由于匿名函数递归调用自身, f
的参数必须接受自身,同时 f
最终也是返回一个 baseFnType
类型,这样才能跟包装器的返回类型一致:
type fnType2 func(fnType2) baseFnType
看看 fnType2
,参数是自己,返回是 baseFnType
类型—— fnType2
也是一个“递归类型”。
func(f fnType2) baseFnType { return f(f) }
最后,如何调用这个匿名递归函数呢?
很容易,把逻辑以匿名函数的方式包装一下传递给这个匿名递归调用即可:
comma := func(f fnType2) baseFnType { return f(f) }(func(f fnType2) baseFnType { return func(s string) string { n := len(s) if n <= 3 { return s } return f(f)(s[:n-3]) + "," + s[n-3:] } })
等等,为何最后有个 return f(f)...
?怎么会是 f(f)
呢?因为前面说了,递归调用的匿名函数现在是 f(f)
。
好了,先来试试看:
fmt.Printf("%v/n", comma("1234567890"))
很神奇,真的输出了:
1,234,567,890
最关键的一步迈出去了。
简化,遵循Scheme十诫之第八戒——“使用辅助函数来抽象表示方式”
看看逻辑混搭在这个函数里也是怪难受的,我们首先来分离逻辑和骨架。
首先把函数调用同逻辑处理做一定的分离:
comma := func(f fnType2) baseFnType { return f(f) }(func(f fnType2) baseFnType { g := func(s string) string { return f(f)(s) } return func(s string) string { n := len(s) if n <= 3 { return s } return g(s[:n-3]) + "," + s[n-3:] } })
然后我们需要为:
return func(s string) string { ... }
里面的业务逻辑定义一个函数,然后通过这个函数,把业务逻辑“注入”进来,这样,以后不同的业务逻辑都能够通过这个函数“注入”进来,内部的骨架就不用调整了。因此,该函数也是个包装器函数。
在此之前我们需要定义一个包装器的函数类型,该类型以 baseFnType
为参数类型——因为我们传入的函数类型是 func(s string) string
,根据 return f(f)(s[:n-3]) + "," + s[n-3:]
,包装器返回的也是一个 baseFnType
类型。因此,我们就有了:
type fnType1 func(baseFnType) baseFnType
以下是简化后的代码:
comma := func(fn fnType1) baseFnType { return func(f fnType2) baseFnType { return f(f) }(func(f fnType2) baseFnType { g := func(s string) string { return f(f)(s) } return fn(g) }) }
来试试代码正确与否:
comma := func(fn fnType1) baseFnType { return func(f fnType2) baseFnType { return f(f) }(func(f fnType2) baseFnType { g := func(s string) string { return f(f)(s) } return fn(g) }) }(func(g baseFnType) baseFnType { return func(s string) string { n := len(s) if n <= 3 { return s } return g(s[:n-3]) + "," + s[n-3:] } }) fmt.Printf("%v/n", comma("1234567890"))
OK!输出:
1,234,567,890
接下来,我们把 g
定义直接放进到 fn
里:
y := func(fn fnType1) baseFnType { return func(f fnType2) baseFnType { return f(f) }(func(f fnType2) baseFnType { return fn(func(s string) string { return f(f)(s) }) }) }
嗯!这就是大名鼎鼎的Y组合子。
来测试一下:
comma := y(func(g baseFnType) baseFnType { return func(s string) string { n := len(s) if n <= 3 { return s } return g(s[:n-3]) + "," + s[n-3:] } }) fmt.Printf("%v/n", comma("1234567890"))
结果OK!
甚至还可以带入不同逻辑,比如:
upper := y(func(g baseFnType) baseFnType { return func(s string) string { n := len(s) if n == 0 { return s } return g(s[:n-1]) + strings.ToUpper(s[n-1:]) } }) fmt.Printf("%v/n", upper("abcdefghijklmnopqrstuvwxyz."))
将输出:
ABCDEFGHIJKLMNOPQRSTUVWXYZ.
至此本文告一段落。
由于动态语言没有强类型要求,因此实现的Y组合子更简单。下面分别列出Python、JavaScript、Scheme的Y组合子形式。
Y = lambda fn: (lambda f: f(f))(lambda f: fn(lambda s: f(f)(s))) Y(lambda g: lambda s: s if len(s)==0 else g(s[:len(s)-1]) + s[len(s)-1].upper())("abcdefghijklmnopqrstuvwxyz.")
输出:
const Y = function(fn) { return (function (f) { return f(f); } (function (f) { return fn(function (s) { return f(f)(s); }); })); } Y(function(g) { return function(s) { let n = s.length; if (n === 0) { return s; } return g(s.substring(0, n-1)) + s.substring(n-1).toUpperCase(); } })("abcdefghijklmnopqrstuvwxyz.")
输出:
(define Y (lambda (fn) ((lambda (f) (f f)) (lambda (f) (fn (lambda (s) ((f f) s))))))) ((Y (lambda (g) (lambda (s) (cond ((null? s) '()) (else (cons (string-upcase (car s)) (g (cdr s)))))))) '("a" "b" "c" "d" "e"))
输出: