Y combinator
Y 組合子
lambda函式是匿名函式,也就是沒有名字的函式,那麼如何用lambda函式實現遞迴呢?
因為python的匿名函式表示方法侷限性很大,所以我使用了scheme語言講解。
下面看一個遞迴求階乘的函式:
(define fac
(lambda (x)
(if (= 0 x)
1
(* x (fac (- x 1))))))
如果不要第一行的命名,那如何遞迴?稍作思考,我們不難想到在lambda函式裡面巢狀一個lambda函式,裡面的函式用外面的函式的引數來遞迴,如下:
(lambda (fac) (lambda (x) (if (= 0 x) 1 (* x (fac (- x 1))))))
問題來了,這個匿名函式需要兩個引數,第一個是fac,第二個是x,x是我們要計算的數字,那fac是什麼?根據最後一行,我們可知fac應該是一個函式,我們賦予它一個什麼函式比較好呢?因為我們在做的是遞迴,所以憑直覺我們把他自己賦值給他自己(對真的就是直覺,畢竟遞迴就是引用自己)。那麼我們得到了下面這個東西:
((lambda (fac) (lambda (x) (if (= 0 x) 1 (* x (fac (- x 1)))))) ((lambda (fac) (lambda (x) (if (= 0 x) 1 (* x (fac (- x 1)))))) (lambda(..)(...)))
通過觀察,我們發現這個函式其實等價於下面這個函式:
((lambda (f)
(f f))
(lambda (fac)
(lambda (x)
(if (= 0 x)
1
(* x (fac (- x 1)))))))
上面的
((lambda (f)
(f f)))
實際上是讓跟在後面的函式呼叫自己,這樣函式被大大簡化,而且不再需要在末尾加上一個不知道應該是什麼的打了省略號的函式(其實真要填充的話還是填它自己,可這樣下去就無窮無盡了)。
此外,如果試著執行一下上面的匿名函式就會發現,這個函式只能算0的階乘,沒有簡化前的也是。如果要算1的階乘,對於未簡化的函式,我們需要在省略號處重複變數為fac的匿名函式,而對於簡化後的函式我們可以這樣寫:
((lambda (f)
(f (f f))) ;;這裡把f多巢狀一層,如果要算2的階乘就(f (f (f f))),以此類推
(lambda (fac)
(lambda (x)
(if (= 0 x)
1
(* x (fac (- x 1)))))))
這樣顯然是不行的,那要怎麼改才行呢?首先我們知道fac和f都是形參,沒有本質區別,所以可以寫成:
((lambda (f)
(f f))
(lambda (f)
(lambda (x)
(if (= 0 x)
1
(* x (f (- x 1)))))))
根據前面提到的算1的階乘的方法,我們得知要想算更大的數,需要讓f呼叫自己,而函式的最後一行中f只是呼叫下一次的數字,所以無法無限迴圈 ,因此有如下改動:
((lambda (f)
(f f))
(lambda (f)
(lambda (x)
(if (= 0 x)
1
(* x ((f f) (- x 1))))))) ;; 變f為(f f)
這樣,這個函式就可以計算任何一個數字的階乘,而且它沒有名字!
這東西被稱為“窮人的Y組合子”,它和真正的Y組合子仍然有差距,它並不具有普遍性,只能算階乘。為了得到更普遍的函式,我們需要繼續抽象。
首先我們考慮到,不管這個函式有什麼功能,它遞迴時一定要有(f f),然後才是參與遞迴的變數。因為這是一個通用的函式,我們把它抽象出來:
(lambda (x)
((f f) x))
這個函式接受一個變數,並且返回(f f)和這個變數,那麼最後一行就可以寫成:
(* x (lambda (x) ((f f) (- x 1)))
想一想,我們最終的目的是什麼?是把fac這個函式從“窮人的Y組合子”裡面剝離出來,而fac的最後一步不就是
(* x (fac (- x 1))
嗎?前面抽象出的那個函式正好可以用在這裡,然後在前面再加上變數為fac的函式,得到:
((lambda (f)
(f f))
(lambda (f)
((lambda (fac)
(lambda (x)
(if (= 0 x)
1
(* x (fac (- x 1))))))
(lambda (x) ((f f) x)))))
在這個函式裡面,第4到第8行的函式和其它部分互不影響,可以抽出來。這樣我們就可以得到下面的函式:
(lambda (func)
((lambda(f)
(f f))
(lambda (f)
(func (lambda (x)
((f f) x))))))
func替代了前面的可剝離部分,現在,我們就得到了所謂的Y組合子。它接受兩個變數,第一個是函式,第二個是用來計算的引數。下面是Y組合子表示的階乘:
(define Y
(lambda (func)
((lambda(f)
(f f))
(lambda (f)
(func (lambda (x)
((f f) x)))))))
;; 上面的函式體中沒有用到Y,Y只是為了方便使用而繫結的
(Y (lambda (fac)
(lambda (x)
(if (= 0 x)
1
(* x (fac (- x 1)))))))
;; 帶入4
((Y (lambda (fac)
(lambda (x)
(if (= 0 x)
1
(* x (fac (- x 1)))))))
4)
;; 執行得到24,正確
現在改寫這個Y組合子,很簡單就可以得到python版:
Y = lambda func: (lambda f: f(f))(lambda f: func(lambda x: f(f)(x)))
Q.E.D