newLISP 宏簡介
我們已經介紹了 newLISP 的基礎知識,但還有很多強大的功能有待發現。一旦掌握了語言的主要規則,就可以決定要新增哪些更高階的工具。你可能想探索的一個功能是 newLISP 提供的宏。
宏是一種特殊的函式型別,你可以使用它來改變 newLISP 對程式碼求值的方式。例如,你可以建立新的控制函式型別,比如你自己的 if 或 case 版本。
使用宏,你可以開始讓 newLISP 按照你的意願工作。
嚴格來說,newLISP 的宏是 fexprs,而不是宏。在 newLISP 中,fexprs 被稱為宏部分是因為說 'macros' 比說 'fexprs' 簡單得多,但主要是它們與其他 LISP 方言中的宏具有相似的目的:它們允許你定義 特殊形式,比如你自己的控制函式。
為了理解宏,讓我們回到本介紹中的第一個例子。考慮這個表示式是如何求值的
(* (+ 1 2) (+ 3 4)) ;-> (* 3 7) ;-> 21
* 函式根本看不到 + 表示式,只看到它們的結果。newLISP 熱情地對加法表示式進行了求值,然後只將結果傳遞給乘法函式。這通常是你想要的,但有時你不想立即對每個表示式求值。
考慮內建函式 if 的操作
(if (<= x 0) (exit))
如果 x 大於 0,則測試返回 nil,因此(exit)函式不會被求值。現在假設你想要定義你自己的 if 函式版本。它應該很容易
(define (my-if test true-action false-action)
(if test true-action false-action))
> (my-if (> 3 2) (println "yes it is" ) (exit)) yes it is $
但這不起作用。如果比較返回 true,newLISP 會列印一條訊息,然後退出。即使比較返回 false,newLISP 仍然會退出,不會列印任何訊息。問題在於(exit)在呼叫 my-if 函式之前就進行求值,即使你不想這樣做。對於普通函式,引數中的表示式首先進行求值。
宏類似於函式,但它們允許你控制何時(以及是否)對引數進行求值。你使用 define-macro 函式來定義宏,就像你使用 define 來定義函式一樣。這兩個定義函式都允許你建立接受引數的函式。重要的區別是,對於普通的 define,引數在函式執行之前進行求值。但是當你呼叫用 define-macro 定義的宏函式時,引數會以原始的未求值形式傳遞給定義。你決定何時對引數進行求值。
my-if 函式的宏版本如下所示
(define-macro (my-if test true-action false-action)
(if (eval test) (eval true-action) (eval false-action)))
(my-if (> 3 2) (println "yes it is" ) (exit))
"yes it is"
test 和 action 引數不會立即求值,只有當你想要它們求值時,才會使用 eval。這意味著(exit)不會在進行測試之前求值。
這種推遲求值的能力使你能夠編寫自己的控制結構,並向語言新增強大的新形式。
newLISP 提供了一些用於構建宏的有用工具。除了 define-macro 和 eval 之外,還有 letex,它提供了一種在對錶達式求值之前將區域性符號擴充套件到表示式中的方法,以及 args,它返回傳遞給宏的所有引數。
編寫宏時需要注意的一個問題是宏中的符號名稱可能會與呼叫宏的程式碼中的符號名稱混淆。這是一個簡單的宏,它向語言添加了一個新的迴圈結構,該結構結合了 dolist 和 do-while。一個迴圈變數在條件為 true 時遍歷列表
(define-macro (dolist-while)
(letex (var (args 0 0) ; loop variable
lst (args 0 1) ; list
cnd (args 0 2) ; condition
body (cons 'begin (1 (args)))) ; body
(let (y)
(catch (dolist (var lst)
(if (set 'y cnd) body (throw y)))))))
它像這樣呼叫
(dolist-while (x (sequence 20 0) (> x 10))
(println {x is } (dec x 1)))
x is 19 x is 18 x is 17 x is 16 x is 15 x is 14 x is 13 x is 12 x is 11 x is 10
它似乎工作良好。但有一個細微的問題:你不能使用名為 y 的符號作為迴圈變數,即使你可以使用 x 或其他任何符號。將一個 (println y) 語句放入迴圈中看看原因
(dolist-while (x (sequence 20 0) (> x 10))
(println {x is } (dec x 1))
(println {y is } y))
x is 19 y is true x is 18 y is true x is 17 y is true
如果你嘗試使用 y,它將無法工作
(dolist-while (y (sequence 20 0) (> y 10))
(println {y is } (dec y 1)))
y is value expected in function dec : y
問題在於 y 被宏用來儲存條件值,即使它在自己的 let 表示式中。它顯示為一個 true/nil 值,因此不能遞減。為了解決這個問題,將宏包含在一個上下文中,並使宏成為該上下文中的預設函式
(context 'dolist-while)
(define-macro (dolist-while:dolist-while)
(letex (var (args 0 0)
lst (args 0 1)
cnd (args 0 2)
body (cons 'begin (1 (args))))
(let (y)
(catch (dolist (var lst)
(if (set 'y cnd) body (throw y)))))))
(context MAIN)
它可以用相同的方式使用,但沒有任何問題
(dolist-while (y (sequence 20 0) (> y 10))
(println {y is } (dec y 1)))
y is 19 y is 18 y is 17
newLISP 使用者發現許多不同的使用宏的原因。以下是我在新LISP 使用者論壇上找到的幾個宏定義。
這是一個稱為 ecase(求值-case)的 case 版本,它確實對測試進行求值
(define-macro (ecase _v)
(eval (append
(list 'case _v)
(map (fn (_i) (cons (eval (_i 0)) (rest _i)))
(args)))))
(define (test n)
(ecase n
((/ 4 4) (println "n was 1"))
((- 12 10) (println "n was 2"))))
(set 'n 2)
(test n)
n was 2
你可以看到,除法(/ 4 4)和(- 12 10)都被求值了。在標準版本的 case 中,它們不會被求值。
這是一個建立函式的宏
(define-macro (create-functions group-name)
(letex
((f1 (sym (append (term group-name) "1")))
(f2 (sym (append (term group-name) "2"))))
(define (f1 arg) (+ arg 1))
(define (f2 arg) (+ arg 2))))
(create-functions foo)
; this creates two functions starting with 'foo'
(foo1 10)
;-> 11
(foo2 10)
;-> 12
(create-functions bar)
; and this creates two functions starting with 'bar'
(bar1 12)
;-> 13
(bar2 12)
;-> 14
以下程式碼更改了 newLISP 的操作,以便使用 define 定義的每個函式在求值時,會將它的名稱和引數詳細資訊新增到日誌檔案中。執行指令碼時,日誌檔案將包含已求值的函式和引數的記錄。
(context 'tracer)
(define-macro (tracer:tracer farg)
(set (farg 0)
(letex (func (farg 0)
arg (rest farg)
arg-p (cons 'list (map (fn (x) (if (list? x) (first x) x))
(rest farg)))
body (cons 'begin (args)))
(lambda
arg
(append-file
(string (env "HOME") "/trace.log")
(string 'func { } arg-p "\n"))
body))))
(context MAIN)
(constant (global 'newLISP-define) define)
; redefine the built-in define:
(constant (global 'define) tracer)
要使用這個簡單的跟蹤器執行指令碼,請在執行之前載入上下文
(load {tracer.lsp})
生成的日誌檔案包含一個列表,其中包含已呼叫的每個函式以及它接收的引數
Time:Time (1211760000 0) Time:Time (1230163200 0) Time:Time (1219686599 0) show ((Time 1211760000 0)) show ((Time 1230163200 0)) get-hours ((Time 1219686599 0)) get-day ((Time 1219686599 0)) days-between ((Time 1219686599 0) (Time 1230163200 0)) leap-year? ((Time 1211760000 0)) adjust-days ((Time 1230163200 0) 3) show ((Time 1230422400 0)) Time:Time (1219686599 0) days-between ((Time 1219686599 0) (Time 1230422400 0)) Duration:Duration (124.256956) period-to-string ((Duration 124.256956)) days-between ((Time 1219686599 0) (Time 1230422400 0)) Duration:Duration (124.256956) Time:print ((Time 1211760000 0)) Time:string ((Time 1211760000 0)) Duration:print ((Duration 124.256956)) Duration:string ((Duration 124.256956))
它會大大減慢執行速度。