Y组合子
Y組合子
?
Y組合子的用處
作者:王霄池鏈接:https://www.zhihu.com/question/21099081/answer/18830200
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
Y組合子的用處是使得 lambda 表達式不需要名字。
如你所說,階乘函數可以這樣定義:let F = lambda n. n==0 ? 1 : n*(F n-1) 當我們需要調用的時候,我們只需要這樣寫就可以了:
F 4
但你有沒有想過,如果我們沒有 let 這個關鍵字怎么辦?沒有 let,就不能對一個 lambda 表達式命名。實際上,在 lambda 演算里確實沒有 let,換句話說,let 只是個語法糖,讓我們寫起來更加舒適而已。沒有 let,并沒有對lambda表達式造成什么實質性的傷害。
數學家們推崇:如無必要,勿增實體
是的。所以,你不能對任何lambda表達式命名。這就像你中了一個沉默魔法一樣。
我們先來看看如果沒有遞歸,無名 lambda 表達式是如何使用的。我們來寫一個求平方的lambda:
lambda x. x * x 這個lambda是無名的。如果要調用,我們只能這么調用:
( lambda x. x * x ) 3
結果自然是返回 9 了。
看來沒有名字,lambda 世界還是可以正常運轉的。且慢,我們不要忘記遞歸。遞歸函數,似乎真的是個問題——如果沒有名字,自身如何調用自身?其實也不是啥大問題。不過,要解決這個問題,我們先假設我們可以使用名字。別擔心,這只是前進途中的曲折。最后,我們會去掉名字的,大家先不要著急。
我們以階乘函數為例,先看看我們現階段的成果:let F = lambda n. n==0 ? 1 : n*(F n-1) 首先我們先設法消除掉 lambda 函數體中的函數名稱(對不起,一激動就用上了函數這個說法,如果你不知道什么叫函數,那么你就可以理解為函數就是 lambda,二者是等同的)。方法就是將函數作為參數傳進去。
let F = lambda f. lambda n. n==0 ? 1 : n*((f f) (n-1)) 這個函數的接受一個參數,返回一個函數,這個返回的函數才是真正做計算的階乘函數。
調用此函數的方法如下:
F F 4
將會返回24。
接下來的一步將是至關重要的。我們現在就拋棄let關鍵字。我們將F的名字換成F的定義,于是調用階乘函數的的方式將變成如下的樣子:( lambda f. lambda n. n==0 ? 1 : n*((f f) (n-1)) ) ( lambda f. lambda n. n==0 ? 1 : n*((f f) (n-1)) ) 4
看到了沒,這里的所有 lambda 都沒有名字,不過,這絲毫沒有影響 lambda 表達式的威力。
如果你看到這里,就會發現,我們可以用類似的方法定義所有的遞歸函數,而用不著Y組合子。是的。你是對的。上面這種方法叫做窮人的Y組合子。但Y組合子的作用就是提供了一個通用的方法來定義遞歸函數。
讓我們來看一下Y的定義:lambda f. (lambda x. (f(x x)) lambda x. (f(x x)))
要講清楚Y的來龍去脈,可是非常難(大家可以去看我的博文重新發明 Y 組合子 Python 版)。事實上,連發現它的哈斯卡大神也感慨不已,覺得自己撿了個大便宜,還因此將Y紋在了自己的胳膊上。我現在就只講Y的用處了。
我們用Y來定義一下遞歸函數
let F = Y ( lambda f. lambda n. n==0 ? 1 : n*(f n-1) )
大家有沒有發現,定義變得比以前的特定方法更加優美了。在之前的特定方法中 f 需要應用于自身,但現在,f 是由 Y 提供的,是一個純階乘函數。
不只是定義更加優雅,連調用也像有名字的lambda一樣優美了。我們現在就優雅的調用階乘函數:F 4 而去掉F的名字,我們有:
( Y ( lambda f. lambda n. n==0 ? 1 : n*(f n-1) ) ) 4 再去掉Y的名字:
( ( lambda f. (lambda x. (f(x x)) lambda x. (f(x x))) )( lambda f. lambda n. n==0 ? 1 : n*(f n-1) ) ) 4
看,這里沒有任何名字,但我們定義并且調用了階乘函數。再次強調一下,階乘函數是個遞歸函數哦。
任何一階遞歸函數都可以用Y來定義,就像我們定義階乘函數那樣:Y (lambda f. < 真正的函數體,在內部用f指代自身 >)
多說一句,可以在 JavaScript 中實現Y算子,如果用上 CoffeScript 提供的語法糖,將非常優雅(這里我原寫錯了,感謝 @Liu Martin ):
Y = (g) ->h = (f) ->g(n) -> f(f) nh h
Y算子真是人見人愛。但除了證明lambda只需要alpha/beta/eta三條規則而不需要命名之外,它主要用自身的優美供大家感嘆。在真實的世界中,不論是數學家,還是函數式編程的 coder,都需要給事物命名。
====更新:函數不動點在編程中的應用 http://www.lfcs.inf.ed.ac.uk/reports/97/ECS-LFCS-97-375/ECS-LFCS-97-375.pdf
?
重新發明 Y 組合子 JavaScript(ES6) 版
關于Y組合子的來龍去脈,我讀過幾篇介紹的文章,相比之下,還是王垠大神的著作?最易懂。但他原來所有的語言是scheme,我寫一個 JS 版的,來幫助大家理解吧。
我們首先來看看如何實現遞歸。
lambda演算的語法非常簡潔,一言以蔽之:
x | t t | λx.t其中xx表示變量,t?tt?t?表示調用,?λx.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) )) )這就是最后的結果了。
名調用中,可以這么寫:
λf.(λu.u?u)(λx.f(x?x))λf.(λu.u?u)(λx.f(x?x))或者使用更經典的形式
λf.(λx.f(x?x))(λx.f(x?x))?
?
淺談Y組合子
來源 http://jjyy.guru/y-combinator
?
這篇文章希望能夠通俗地講清楚Y組合子,如果對lambda演算感興趣的同學可以看看最后的相關資料
在lambda中,如果我們想要遞歸,以斐波那契數列為例,可以這樣:
let power = lambda n. IF_Else n==0 1 n*power(n-1)然而,在“純”lambda演算中,是沒有let關鍵字的,但我們可以暫時忘記這件事。我們需要換個方法進行遞歸,如果直接的遞歸不可行,那么我們可以嘗試間接的。很容易能想到通過參數把自己傳給自己:
let P = lambda self n. If_Else n==0 1 n*self(self, n-1) P(P, 3)如果每次遞歸都要這么寫,就顯得很不優雅。我們要想一個辦法,能夠通用的把自己傳給自己。就像上面一樣。我們試著構造一下,把斐波那契數列的邏輯替換為任意函數:
let gen = lambda self. AnyFunction(self(self)) gen(gen)嘗試寫出斐波那契數列的AnyFunction實現:
let AnyFunction = lambda self n. If_Else n==0 1 n*self(n-1)經過展開之后,發現任何函數只要在AnyFunction那個位置,經過上面的代碼之后,都能夠實現遞歸。
其中gen(gen)展開如下:
gen(gen) => AnyFunction(gen(gen))可能你會疑問,gen(gen)為什么能夠表達自己呢?因為gen(gen)展開為AnyFunction(gen(gen)),它能夠返回AnyFunction自身,這就得到自己了。并且這時會把這個gen(gen)再傳給AnyFunction。而gen(gen)不求值時是不展開的,因此gen(gen)沒有被調用時,沒有任何作用,但是一旦AnyFunction內部調用了傳進來的gen(gen),那么就進行求值再次得到“自己”。通俗來講,與其說gen(gen)是自身,還不如說這是一個把能夠得到自己,并且把gen(gen)再次傳入的函數。
在理解這個機制之后,通用的遞歸函數已經到手。封裝一下就輕而易舉了,這就是傳說中的Y組合子:
let Y = lambda f. let gen = lambda self. f(self(self)) gen(gen)再把let去掉可得到Y的定義:
lambda f. (lambda x. (f(x x)) lambda x. (f(x x)))接下來可以試著使用一下:
( ( lambda f. (lambda x. (f(x x)) lambda x. (f(x x))) ) ( lambda f. lambda n. n==0 ? 1 : n*(f n-1) ) ) 4看,完美!證明了lambda只需要alpha/beta/eta三條規則而不需要命名。
相關資料,從易到難排序
- g9的lambda calculus系列?有很多lambda的入門講解,幽默風趣
- The Little Schemer?手把手學lambda
- The Seasoned Schemer?手把手2
- SICP?不用多說,看書評
- MIT講SICP?MIT的課,值得一看
其他相關資料
- 康托爾、哥德爾、圖靈——永恒的金色對角線?講了圖靈機的起源--對角線法
- 對角線方法之后的故事?關于對角線法的誤用
- 計算的本質?手把手用Ruby講圖靈機,比較有趣,通俗易懂
- 圖靈的秘密?通俗易懂,引用圖靈論文,有理有據。圖靈機紙條部分比較枯燥
?
==================?End
?
總結
- 上一篇: 33、Map简介
- 下一篇: 使用Nexus配置Maven私有仓库