一次造輪子的經歷
因爲我的Scheme入門書是Dan Friedman的《The Little Schemer》,老爺子從頭到尾都沒有提到過if
。所以一直到翻完整本書,對於只有2個分支的邏輯,我也是用這麼“蠢萌”的方式寫的(這裏的cond
可以理解成if
的多分支的版本):
(define (my-abs x)
(cond ((>= x 0) x)
(else (- x))))
所謂殺雞焉用牛刀,既然“if-then-else”的簡潔性擺在那裏,造輪子的事肯定也是幹過的:
(define (my-if predicate then-clause else-clause)
(cond (predicate then-clause)
(else else-clause)))
通過自定義my-if
函數,我們自己實現了if
的功能。來測試一下:
(my-if (= 1 2) 0 1)
;Value: 1
(my-if (= 1 1) 0 1)
;Value: 0
scheme的波蘭表達式寫法,讓自定義函數和默認關鍵字之間沒有明顯的區分。接下來就可以把最開始的my-abs
改寫的簡短一點:
(define (my-abs x)
(my-if (>= x 0) x (- x)))
在很長一段時間裏這個my-if
都可以很好的運行,直到有一天...
應用序和正則序
事情是這樣的:之前替換的都是比較簡單的場景,而這一次在cond
中存在一個遞歸的函數調用,當再次運行的時候,發現程序無法退出了。下面是這個例子的簡化版本,原始版本參考SICP[1]的32頁。
; 某個遞歸調用自己的函數
(define (bomb) (bomb))
; 某個可以正常執行的代碼片段
(cond ((= 1 1) 1)
(else (bomb)))
; 陷入無限遞歸
(my-if (= 1 1) 1 (bomb))
讓我們在仔細審視一下cond
的求值過程:解釋器會按順序判斷謂詞邏輯是否成立,一旦(= 1 1)
返回的結果爲true
,那麼立刻返回1
,並且 忽略 後續所有的謂詞和對應的表達式,也就是說(bomb)
根本沒機會執行。
而my-if
其實是一個函數調用,遵循2種可能的求值方式:應用序(applicative-order)或者正則序(normal-order)。
先看一個直觀一點的例子[2]:
(define (double x)
(+ x x))
(double (+ 2 1))
對於應用序,會首先求出(+ 2 1)
的值,然後帶入double
的定義中,也就是說+
一共執行了 2 次,1次是計算(+ 2 1)
,1次是計算(+ 3 3)
;
對於正則序,則會先展開double
的定義,整個表達式變成(+ (+ 2 1) (+ 2 1))
,然後對於(+ 2 1)
分別求值 2 次,最後再相加,整個過程中+
一共執行了 3 次。
再回過頭看之前所遇到的問題,如果採用正則序,在遇到my-if
時會先將其展開,然後再將參數帶入,也就是和之前直接用cond
寫的沒有區別。而應用序則相反,會先對於參數求值,然後再執行函數調用。也就是說,無論my-if
是用什麼方式定義的,(= 1 1)
、1
、(bomb)
都會被先求值,而當(bomb)
被求值時程序就陷入無限遞歸了。
我知道的幾乎所有的編程語言(包括流行的Java、Python,也包括不太流行的Scheme)都選擇了應用序作爲求值方式。這也導致了我之前甚至連什麼是正則序都不知道(至於正則序、惰性求值之類易混淆的概念,後面有機會再寫一篇)。
再造一次輪子
在這次失敗的經歷後的某天,結合另一個我之前一直想不明白的問題:宏到底有什麼作用?我突然就經歷了一個“尤里卡時刻”:爲什麼不試試用宏來重新定義my-if
!
(define-syntax my-if
(syntax-rules ()
((my-if predicate then-clause else-clause)
(cond (predicate then-clause)
(else else-clause)))))
通過define-syntax
我們定義了一個宏,語法的部分我不做贅述。可以看到,通過把my-if
展開爲cond
對應的表達式,我們實現了需要的功能,因爲宏展開是基於正則序的。
順帶一提的是,我們通過宏,幾句話就實現了自定義語法,這也正是Lisp系語言元編程的能力。