簡單易懂的程序語言入門小冊子(4):基於文本替換的解釋器,遞歸,如何構造遞歸函數,Y組合子


遞歸。哦,遞歸。 遞歸在計算機科學中的重要性不言而喻。 遞歸就像女人,即令人煩惱,又無法拋棄。

先上個例子,這個例子里的函數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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM