Scheme宏的二三事

一次造輪子的經歷

因爲我的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系語言元編程的能力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章