天天看點

Lisp 匿名遞歸函數

主流的 lisp 實作(clisp、guile、emacs lisp 等)中預設都沒提供定義匿名的遞歸函數的方法。上 google 搜尋了一下,看到不少人也都在抱怨。不過 lisp 一個特色就是你可以自己動手添加需要的語言特性!于是我就嘗試着自己寫一個宏來實作這個功能。

方法就是額外添加一個參數,用于傳遞函數本身。個人覺得這個方法不夠優雅,如果需要定義多個匿名的遞歸函數,備援的代碼也會相當多。不過它已經反映出實作匿名遞歸函數的本質方法:将匿名函數賦給一個臨時變量。

我的想法是在定義 lambda 函數時使用 this 來代表自身。例如編寫一個用遞歸實作的匿名階乘函數:

根據上述期望的代碼,目标是:

定義一個能建立并傳回匿名函數的宏; 這個匿名函數要綁定到 this 符号(symbol)上。

我一開始先嘗試用 emcas lisp 來實作。實作第一個目标很簡單,隻要将宏的參數和 lambda 這個符号連結起來即可:

第二個目标可以使用 defalias。它能将符号綁定到函數空間裡(setq 隻能綁定到變量空間,是以通過 setq 綁定的函數隻能用 funcall 來調用):

這樣就擁有了符合上述要求,可以定義匿名遞歸函數的方法了。用這個宏來編寫階乘函數并執行:

執行結果正确,實作的代碼也很簡短,看起來很不錯的樣子。可惜它有個不足之處:emacs lisp 中有 let 等函數來定義局部變量,卻沒用類似的方法來定義局部函數,而且也不支援閉包,是以 this 這個符号是所有匿名函數共用的。如果同時用 re-lambda 定義了兩個匿名函數,前一個函數就會被覆寫:

好在 common lisp 中提供的 labels 可以定義局部函數,有了它就不用擔心命名空間被污染了:

上面代碼中,因為 this 是作為傳回的匿名函數中的一個局部函數,是以不必擔心命名沖突問題。你可能會想,為什麼不用 re-lambda 代替 this?這樣也能避免 this 和其他全局變量或函數沖突。即:

這看起來很不錯:lambda 在不同的上下文裡表現出不同的角色。而且實作的方法也很簡單,隻要将上述代碼中的 this 替換成 re-lambda 即可。但這會引起另一個問題:不能定義嵌套匿名遞歸函數。例如,求 1 到 n 所有階乘的和:

如果沒用 this 的話,就很難分辨哪些 re-lambda 是定義函數,哪些是計算結果。

最後,還有一個遺憾:你可能也發現了,我給的期望結果裡調用 lambda 都不需要 funcall。因為它是按 scheme 的文法寫的,而實作的代碼是按 common lisp 的文法寫的。就這方面來說,我覺得 scheme 的文法更優雅些。