重新發明 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) ))
)
這就是最後的結果了。
名呼叫中,可以這麼寫:
或者使用更經典的形式
===============
我們講完了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) )) )