2025-05-25

一直在拖延,玩Lisp,終究繞不開這道坎啊。既然如此,抖擻下精神,開始瞭。

作者首先又是抱怨、比較瞭一番lisp的宏與其他語言的區別。從形式上來講,其實所謂的標準宏就和c,java之類的類庫一樣,是事先已經被完成、驗證過的東東封裝在編譯/虛擬環境 中供coder使用(省的重復造輪子)。當然,在lisp中,宏可不單單是如此簡單,不過~管他呢(拜托,這隻是初學者教程!)在這一章節主要給大夥介紹幾個常用的宏。


條件宏
最常見的估計算是"if"瞭:
Java代碼 
(if condition then-form [else-form]) 
 
(if (> 2 3) "Yup" "Nope") ==> "Nope" 
(if (> 2 3) "Yup")        ==> NIL 
(if (> 3 2) "Yup" "Nope") ==> "Yup" 

真是簡單易懂啊,不過if隻支持單行動語句,也就是說,“無論條件是否成立,都隻能做一件事,而不能做一組事”。而在真實環境中,自然會大大影響其使用面。為此就引入瞭特別造作符:PROGN
Java代碼 
(if (spam-p current-message) 
    (progn 
      (file-in-spam-folder current-message) 
      (update-spam-database current-message))) 

恩,看起來還不錯,不過,如果有更簡潔的方案呢:
Java代碼 
(when (spam-p current-message) 
  (file-in-spam-folder current-message) 
  (update-spam-database current-message)) 

when也是一個標準宏,即使不是,定義也不麻煩:
Java代碼 
(defmacro when (condition &rest body) 
  `(if ,condition (progn ,@body))) 

它的共生兄弟unless則是對條件作瞭逆判斷:
Java代碼 
(defmacro unless (condition &rest body) 
  `(if (not ,condition) (progn ,@body))) 

似乎沒什麼神奇的啊,即沒有神秘的魔法也沒有暈人的邏輯。對頭!或許"大道至簡"可以用作解釋吧。
如果說when解決瞭執行條目的問題,那麼宏cond則是解決瞭條件條目的問題:
Java代碼 
(cond 
  (test-1 form*) 
      . 
      . 
  (test-N form*)) 
 
(cond (a (do-x)) 
      (b (do-y)) 
      (t (do-z))) '理論上來說最後一條是default 

在做條件運算時,通常都會搭配些基本的運算符:and,or,not(not其實算是函數)。
Java代碼 
(not nil)             ==> T 
(not (= 1 1))         ==> NIL 
(and (= 1 2) (= 3 3)) ==> NIL   '遇到第一個nil即返回,沒有則t 
(or (= 1 2) (= 3 3))  ==> T     '遇到第一個t即返回,沒有則nil 


循環宏
最簡單的兩個循環宏莫過於dolist和dotimes瞭:
Java代碼 
(dolist (var list-form) 
  body-form*) 
 
(dolist (x '(1 2 3)) (print x)) 



NIL 
 
CL-USER> (dolist (x '(1 2 3)) (print x) (if (evenp x) (return))) 


NIL          '通過return截斷瞭循環 
 
—————————– 
(dotimes (var count-form) 
  body-form*) 
 
(dotimes (i 4) (print i)) 




NIL 

一個循環列表,一個根據數值遞進循環,都算定制型的循環宏。而他們都有一個"父宏"(可以這麼叫麼)–do,這玩意由於超級靈活(萬金油啊),那麼也造成瞭易用度的降低:
Java代碼 
(do (variable-definition*) 
    (end-test-form result-form*) 
  statement*) 
 
variable-definition–>(var init-form step-form) 

簡單看來,需要在veriable-definition中定義循環初值,跳步幅度(沒有則默認一步),當達到end-test-form時,執行result-form,他的返回值將作為整個循環的返回值。而statement則是循環體。這裡給出瞭一個例子來證明do的靈活性:
Java代碼 
(do ((n 0 (1+ n)) 
     (cur 0 next) 
     (next 1 (+ cur next))) 
    ((= 10 n) cur)) 

其實就是傳說中的“斐波納契數列”。在這個例子中循環體似乎並不重要,重要的是構造本身。正如前面所說,靈活的同時易用性正在喪失,例子中的括弧實在令人印象深刻啊。咱們來看個對比:
Java代碼 
(do ((i 0 (1+ i))) 
    ((>= i 4)) 
  (print i)) 
 
(dotimes (i 4) (print i)) 

果然是“紅花需要綠葉襯”~
在某些情況下,循環條件都是非必需的:
Java代碼 
(do ()   '雖然沒有條件,位置還得留著 
    ((> (get-universal-time) *some-future-date*)) 
  (format t "Waiting~%") 
  (sleep 60))  

接下來要介紹的算是在lisp界頗具爭議的"強力"循環宏:loop.
dolist,dotimes雖然簡單,覆蓋面卻不廣,而do又過於靈活(=復雜?),那麼看似更趨向於中間的loop便有瞭出現的理由,縱然有不少人反對他(支持的也不少)。
看起來很簡潔:
Java代碼 
(loop 
  body-form*) 

用起來也不賴:
Java代碼 
(loop 
  (when (> (get-universal-time) *some-future-date*) 
    (return)) 
  (format t "Waiting~%") 
  (sleep 60)) 

與do比較下:
Java代碼 
(do ((nums nil) (i 1 (1+ i))) 
    ((> i 10) (nreverse nums)) 
  (push i nums)) ==> (1 2 3 4 5 6 7 8 9 10 
 
(loop for i from 1 to 10 collecting i) ==> (1 2 3 4 5 6 7 8 9 10) 

也來實現個“斐波納契數列”:
Java代碼 
(loop for i below 10 
      and a = 0 then b 
      and b = 1 then (+ b a) 
      finally (return  a)) 

雖然其中用到瞭類似for,below什麼的loop專用符號,但的確看著清爽多瞭,也更符合自然語言。這是loop簇擁們的理由,也是反對者腹誹的地方"一點都不lisp".個人觀點就是,你覺著哪個爽就用哪個唄,正是"條條大路通羅馬"~當然,這裡的loop使用還隻是冰山一角,後面的章節還會詳述。

lisp編譯器自帶的標準宏自然不光這麼幾個,僅僅是摘出幾個常用的,以簡單一窺其中奧妙。那麼,在接下來的章節中,即將深入介紹–自定義宏。你準備好瞭麼?

(未完待續)

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *