遞歸。哦,遞歸。 遞歸在計算機科學中的重要性不言而喻。 遞歸就像女人,即令人煩惱,又無法拋棄。
先上個例子,這個例子里的函數double輸入一個非負整數$n$,輸出$2n$。 \[ {double} = \lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ({double} \; (- \; n \; 1)))) \]
現在的問題是,這個遞歸函數在我們的語言里沒法直接定義。 我說的直接定義是指像這個用let表達式: \[ ({let} \; {double} \; \lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ({double} \; (- \; n \; 1)))) \; M) \] 把這個let表達式宏展開會看得更清楚些: \[ (\lambda {double}.M \; \lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ({double} \; (- \; n \; 1))))) \] $\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ({double} \; (- \; n \; 1))))$ 里的double是個自由變量。 解釋器求值到這里的時候,根本不知道double指的是什么函數。
如何構造遞歸函數
獲得遞歸的一個關鍵是如何在函數體中找到自己(結合一開始的比喻,這句話好像蘊含了其他意義深遠的意思)。 一個簡單的方法是在double上增加一個參數(一般就是第一個參數),把自己傳入參數。 把這個修改后的函數叫做mkdouble1吧。 先不考慮mkdouble1的定義,先觀察mkdouble1的行為。 因為調用mkdouble1要把自己作為第一個參數傳入,所以調用遞歸函數應該這樣寫: \[ (({mkdouble1} \; {mkdouble1}) \; n) \] 也就是說,double就是$({mkdouble1} \; {mkdouble1})$。 \begin{eqnarray*} {double} &=& ({mkdouble1} \; {mkdouble1}) \\ &=& (\lambda v.(v \; v) \; {mkdouble1}) \end{eqnarray*} 最后一步變換是為了讓mkdouble1只出現一次。
現在來考慮mkdouble1的定義。 在double上增加一個參數$f$: \[ {mkdouble1} = \lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ({double} \; (- \; n \; 1)))) \] 函數調用的時候傳入參數$f$的是mkdouble1。 也就是說$f$代表的是mkdouble1。 因此,函數體里遞歸調用的double用$(f \; f)$替換: \[ {mkdouble1} = \lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ((f \; f) \; (- \; n \; 1)))) \] 所以double的定義是: \[ {double} = (\lambda v.(v \; v) \; \lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ((f \; f) \; (- \; n \; 1))))) \] 這個定義可以用之前實現的解釋器運行。 測試一下:
'(let double ((lambda v (v v))
(lambda f
(lambda n
(if (iszero n) 0 (+ 2 ((f f) (- n 1)))))))
(double 4))
>> 8
Y組合子
這一小節比較理論,知道個思路就行了。所以我就隨便寫寫。 好學的人可以自己查資料(Programming Languages and Lambda Calculi, The Little Schemer)。
mkdouble1並不能讓人很滿意,因為它不優雅(都是時臣的錯)。 mkdouble1遞歸調用的地方用的是$(f \; f)$,而比較好看比較符合直覺的應該只有一個$f$。 定義這個所謂的比較好看的函數mkdouble如下: \[ {mkdouble} = \lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; (f \; (- \; n \; 1)))) \] 我們希望能從mkdouble得到遞歸函數double。 這是能做到的。只要在利用mkdouble1的double定義上做幾個簡單的推導就行了: \begin{eqnarray*} {double} &=& (\lambda v.(v \; v) \; \lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; ((f \; f) \; (- \; n \; 1))))) \\ &=& (\lambda v.(v \; v) \; \lambda f.(\lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; (f \; (- \; n \; 1)))) \; (f \; f))) \\ &=& (\lambda x.(\lambda v.(v \; v) \; \lambda f.(x \; (f \; f))) \; \lambda f.\lambda n.({if} \; ({iszero} \; n) \; 0 \; (+ \; 2 \; (f \; (- \; n \; 1))))) \\ &=& (\lambda x.(\lambda v.(v \; v) \; \lambda f.(x \; (f \; f))) \; {mkdouble}) \end{eqnarray*}
$\lambda x.(\lambda v.(v \; v) \; \lambda f.(x \; (f \; f)))$被稱作Y組合子,記為Y。 然后有: \[ {double} = ({Y} \; {mkdouble}) \]
Y組合子可以用來構造遞歸函數。 不過上面的定義在call-by-value的調用方式下會進入無限循環。 具體原因就不講了,只講結論:問題出在$(f \; f)$這里,對$(f \; f)$做一個$\eta$逆歸約就行了。 修改后的Y組合子記為${Y}_{v}$: \[ {Y}_{v} = \lambda x.(\lambda v.(v \; v) \; \lambda f.(x \; (\lambda u.((f \; f) \; u)) \]
測試一下。 Call-by-value的測試:
'(let Y (lambda x
((lambda v (v v))
(lambda f (x (lambda u ((f f) u))))))
(let mkdouble (lambda f
(lambda n
(if (iszero n)
0
(+ 2 (f (- n 1))))))
((Y mkdouble) 4)))
>> 8
Call-by-name的測試:
'(let Y (lambda x
((lambda v (v v))
(lambda f (x (f f)))))
(let mkdouble (lambda f
(lambda n
(if (iszero n)
0
(+ 2 (f (- n 1))))))
((Y mkdouble) 4)))
>> 8
