10 Macros
Lisp code is expressed as lists, which are Lisp objects This makes it possible to write programs that would write programs This lecture shows how to cross the line from expressions to code
How to generate expressions? Just call list How to treat expressions as code? Use the function EVAL > (eval ’( )) 6 > (eval ’(format t “Hello”)) Hello NIL
(defun our-toplevel ( ) (do ( ) (nil) (format t “~%> “) (print (eval (read))))) The toplevel is also known as a read-eval-print loop → 把所讀到的東西給 evaluate 出來
Calling eval is one way to cross the line between lists and code. However, it’s not a very good way: It’s inefficient The expression is evaluated with no lexical context ▪ > (let ((x 1)) (eval ’(+ x 3))) *** - EVAL: variable X has no value
The most common way to write programs that write programs is by defining macros (defmacro nil! (x) (list ’setf x nil)) ; 用 list 來組一個 list A call of the form (nil! a) will be translated into (setf a nil) > (nil! x) NIL > x NIL
To test a function, we call it, but to test a macro, we look at its expansion > (macroexpand-1 ’(nil! x)) (SETF X NIL) T A macro call can expand into another macro call. When the compiler (or toplevel) encounters a macro call, it simply keeps expanding it until it is no longer one ( 展開到沒有為止 )
When you use defmacro, you’re defining a function much like: (lambda (expr) (apply #’(lambda (x) (list ’setf x nil)) (cdr expr))) 若 input 是 (nil! a) 則會 return (setf a nil)
Like a regular quote, a backquote alone protects its argument from evaluation > ‘(a b c) (A B C) The advantage of backquote is: within a backquote expression, you can use, (comma) (comma-at) to turn evaluation back > (setf a 1 b 2) 2 > ‘(a is,a and b is,b) (A IS 1 AND B IS 2)
By using backquote instead of a call to list, we can write macro definitions that look like the expressions they will produce (defmacro nil! (x) ‘(setf,x nil)) > (setf lst ’(a b c)) (A B C) > ‘(lst is,lst) (LST IS (A B C)) > ‘(its elememts (ITS ELEMENTS ARE A B C)
> (setf x ’(1 2)) (1 2) > ‘(x,x (X (1 2) ’(1 2) 1 2) (defmacro test (x) (setf x nil)) (test a) → 會被展開為 NIL > (macroexpand ’(test a)) NIL; T (defmacro test2 (x) ’(setf x nil)) > (macroexpand ’(test2 a)) (SETF X NIL); T > (test2 a) NIL > a *** - EVAL: variable A has no value > x NIL
Comma-at is useful in macros that have rest parameters representing, for example, a body of code Suppose we want a while macro that will evaluate its body so long as an initial test expression remains true > (let ((x 0)) (while (< x 10) (princ x) (incf x))) NIL We can define such a macro by using a rest parameter to collect a list of the expressions in the body, then using comma-at to splice this list into the expansion: (defmacro while (test &rest body) ‘(do ( )
想寫一個程式, 可以 evaluate 他的 body n 次 例如 : > (ntimes 10 (princ “.”)) ………. NIL 如果這樣寫 (defmacro ntimes (n &rest body) ‘(do ((x 0 (+ x 1))) ((>= 看起來正確, 但是某種情況下會有問題
若 macro 裡頭的變數名稱跟原來程式中的衝到, 例如 : > (let ((x 10)) (ntimes 5 (setf x (+ x 1))) x) 10 (let ((x 10)) (do ((x 0 (+ x 1))) ((>= x 5)) (setf x (+ x 1))) x)
解決方式 : 用 gensym (defmacro ntimes (n &rest body) (let ((g (gensym))) ‘(do ((,g 0 (+,g 1))) 但是, 另一個問題 : 如果第一個參數 n 用一個 expression 取代, 那麼 每次做到有 n 地方, 都會 evaluate 一次, 例如 : 若第一個參數為 (setf v (- v 1)), 亦即, > (let ((v 10)) (ntimes (setf v (- v 1)) (princ “.”))) ….. NIL 我們本來希望有 9 個點, 現在只剩 5 個
因為前述的 macro 會被展成 : (let ((v 10)) (do ((#:g1 0 (+ #:g1 1))) ((>= #:g1 (setf v (- v 1)))) (princ “.”))) 迴圈每次做到 ((>= #:g1 (setf v (- v 1)))) v 的值都會變化, 因此 #:g1 不再是跟 9 比較 因此, 我們修正為 : (defmacro ntimes (n &rest body) (let ((g (gensym)) (h (gensym))) ‘(let ((,h,n)) ; 先把 n 算好給 h, 使得迴圈中不會動到 (do ((,g 0 (+,g 1)))
看看 Common Lisp 裏頭內建的 macro > (pprint (macroexpand-1 ’(cond (a b) (c d e) (t f)))) (IF A B (IF C (PROGN D E) F)) pprint 可以以縮排形式將 code 印出
(defmacro cah (lst) ‘(car,lst)) ; 如果 macro 的定義中有可以被 setf 的 function > (let ((x (list ’a ’b ’c))) (setf (cah x) 44) ; 則 macro 也可以被 setf x) (44 B C)
寫一個東西來計算平均 > (avg 2 4 8) 14/3 寫成 function ▪ (defun avg (&rest args) (/ (apply #’+ args) (length args))) 寫成 macro ▪ (defmacro avg (&rest args) ‘(/ args))) 若用 function,args 的長度是在 run-time 時計算, 而若用 macro, 則在 compile-time 即計算完成
Homework ▪ Imagine that the designers of LISP had forgotten to include the UNLESS primitive. Now you decide to define your own UNLESS primitive, which you call WHEN- NIL. Define WHEN-NIL such that it translates (when-nil ) into (when (not ) )
(defmacro when-nil (trigger result) ‘(when (not,trigger),result)) (defmacro when-nil (trigger &rest result) ‘(when