小池有话说

重新发明 Y 组合子 JavaScript(ES6) 版

2015-03-31

关于Y组合子的来龙去脉,我读过几篇介绍的文章,相比之下,还是王垠大神的著作 最易懂。但他原来所有的语言是scheme,我写一个 JS 版的,来帮助大家理解吧。

我们首先来看看如何实现递归。

lambda演算的语法非常简洁,一言以蔽之:

x | t t | λx.t

其中$x$表示变量,$t \ t$ 表示调用, $\lambda x.t$ 表示函数定义。

首先我们来定义一个阶乘函数,然后调用它。

fact = n => n == 1 ? 1 : n * fact(n-1)
fact(5)

lambda演算中不可以这么简单的定义阶乘函数,是因为它没有 = 赋值符号 。

现在我们看到在lambda定义中,存在fact的名字,如果我们想要无名的调用它,是不行的。如下

(n => n == 1 ? 1 : n * fact(n-1))(5) # there is still `fact` name

我们想要将名字消去,如何消去一个函数的名字呢?

首先,没有名字是无法定义一个递归函数的。

那么,我们不禁要问了,哪里可以对事物命名呢?

对了,将之变为参数,因为参数是可以随意命名的。

fact = (f, n) => n == 1 ? 1 : n * f(f, n-1)
fact(fact, 5)

嗯,很好,看起来不错。不过,要记住在 lambda 演算里面,函数只能有一个参数,所以我们稍微做一下变化。

fact = f => n => n == 1 ? 1 : n * f(f)(n-1)
fact(fact)(5)

你可能会说我在做无用功,别过早下结论,我们只需要将 fact 代入,就得到了完美的匿名函数调用。

(f => n => n == 1 ? 1 : n * f(f)(n-1)) (f => n => n == 1 ? 1 : n * f(f)(n-1)) (5)

看,我们成功了,这一坨代码,是完全可以运行的哦。这个叫做 穷人的Y组合子。可以用,但是不通用,你要针对每个具体函数改造。

于是我们继续改造。我们将把通用的模式提取出来,这个过程叫做 抽象

首先我们看到了 f(f) 两次, fact(fact) 一次,这种pattern重复了3次,根据 DRY 原则,我们可以这么做

w = f => f(f)
w(fact) (5) # short version
w (f => n => n == 1 ? 1 : n * f(f)(n-1)) (5) # longer version

现在,我们就只有一个重复的模式了,那就是 f(f) 。但是因为它在函数内部(也就是在业务逻辑内部),我们要先把它解耦出来。也就是 factor out。

我们从 f => n => n == 1 ? 1 : n * f(f)(n-1) 开始

f =>
    n => n == 1 ? 1 : n * f(f)(n-1)

我们令 g=f(f) ,然后 可以变成

f =>
    (g => n => n == 1 ? 1 : n * g(n-1)) ( f(f) )

当然, f(f) 在call by value 时会导致栈溢出,所以我们就 $\eta$ 化一下

f =>
    (g => n => n == 1 ? 1 : n * g(n-1)) ( v => f(f)(v) )

我们看到了 g => n => n == 1 ? 1 : n * g(n-1) 这个就是我们梦寐以求的阶乘函数的定义啊。

我们将这个(阶乘函数的定义)提取出来(再一次的factor out),将之命名为 fact0(更接近本质的fact)。上面的可以改写成。

( fact0 => f =>
    fact0 ( v => f(f)(v) )
) ( g => n => n == 1 ? 1 : n * g(n-1) )

不要忘记最初的w,那么如下:

w(
    (fact0 => f => fact0 ( v => f(f)(v) ))
    (g => n => n == 1 ? 1 : n * g(n-1))
)(5)

很自然我们会再一次把阶乘函数的定义factor out出来,当然,fact0 => f => fact0 ( v=>f(f)(v) ) 中的fact0参数我们也会换成其他的名字,比如 s,而那个fact0的实参,那一大坨更加本质的定义我们也会抽象成一个参数,h

(h =>
    w( (s => f => s ( v => f(f)(v) )) (h))
)
(g => n => n == 1 ? 1 : n * g(n-1)) (5)

好,大功告成,上面的那个括号里面的就是Y了。我们将之单独拿出来看。

(h =>
    w(
        (s => f => s ( v => f(f)(v) )) (h)
    )
)

最中间一行的 h 可以apply一下,也就是化简:

(h =>
    w(
        (f => h ( v => f(f)(v) ))
    )
)

当然, w这个名字也可以去除

(h =>
    (f => h ( v => f(f)(v) ))
    (f => h ( v => f(f)(v) ))
)

这就是最后的结果了。

名调用中,可以这么写:

或者使用更经典的形式