1. 程式人生 > >重新發明 Y 組合子 JavaScript(ES6) 版

重新發明 Y 組合子 JavaScript(ES6) 版

重新發明 Y 組合子 JavaScript(ES6) 版

王霄池

王霄池

時間的逆行者 / 精神共產黨

​關注他

13 人讚了該文章

Y組合子像是一個珍玩,見過它的人大概都愛不釋手。題圖的這個手臂就是他的發明者Curry Haskell,他將它紋在了身上。

關於Y組合子的來龍去脈,我讀過幾篇介紹的文章,相比之下,還是王垠大神的著作 最易懂。但他原來所有的語言是 scheme,我寫一個 JS 版的,來幫助大家理解吧。

我們首先來看看如何實現遞迴。

λ 演算的語法非常簡潔,一言以蔽之:

 

x | t t | λx.t

其中 x 表示變數,t t 表示呼叫, λ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 時會導致棧溢位,所以我們就 η 化一下

 

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) ))
)

這就是最後的結果了。

名呼叫中,可以這麼寫:


\lambda f.(\lambda u. u \ u) (\lambda x. f (x \ x))

或者使用更經典的形式


\lambda f. (\lambda x. f (x \ x)) (\lambda x. f (x \ x))

===============

我們講完了Y的推導過程(非嚴格),我們來證明一下,Y(f)確實是函式 f 的不動點。也就是如同 MIT CS系徽上所言

(Y F) = (F (Y F))

接下來的推導為了方便使用ES6

首先來推導公式的左邊

(Y F)
=
((h =>
    (f => h ( v => f(f)(v) ))
    (f => h ( v => f(f)(v) ))
) F)
= 
    (f => F ( v => f(f)(v) ))
    (f => F ( v => f(f)(v) ))
=
F( (f => F ( v => f(f)(v) )) (f => F ( v => f(f)(v) )) )

然後來推導公式的右邊

(F (Y F) )
=
F ( (f => F ( v => f(f)(v) ))
    (f => F ( v => f(f)(v) )) )