common Lisp学习笔记(十四)
14 Macros
宏通常通过defmacro
来定义,它定义了怎样"翻译"出一个函数调用。 我们定义一个宏的时候说明一个函数调用应该翻译成什么,这个翻译称为宏展开(macro-expansion),由编译器自动 完成。因为宏能”翻译“出能执行的函数,所以这样可以写出能写程序的程序
nil!函数将其参数设为nil
(defmacro nil! (x) (list 'setf x nil))
可以这样理解,(list 'setf x nil)先翻译成一个正确的lisp表达式(setf a nil),然后进行eval操作执行这句话, 将a设为nil。需要注意的是(list 'setf x nil)翻译的时候没有对x进行eval,因为macro是不对参数进行eval操作的
要测试一个宏,可以看它的展开式expansion,函数macroexpand-1
接受一个宏参数,产生展开式
> (macroexpand-1 '(nil! x)) (setf x nil) t
一个宏调用可以翻译为另一个宏调用,这时候编译器会持续的翻译它,直到不能再展开为止
toolkit: ppmx
ppmx: Pretty Print Macro eXpansion
(defmacro ppmx (form) "Pretty prints the macro expansion of FORM." `(let* ((exp1 (macroexpand-1 ',form)) (exp (macroexpand exp1)) (*print-circle* nil)) (cond ((equal exp exp1) (format t "~&Macro expansion:") (pprint exp)) (t (format t "~&First step of expansion:") (pprint exp1) (format t "~%~%Final expansion:") (pprint exp))) (format t "~%~%") (values)))
> (ppmx (incf a)) (setq a (+ a 1))
14.4 defining a macro
(defmacro simple-incf (var) (list 'setq var (list '+ var 1))) > (ppmx (simple-incf a)) macro expansion: (setq a (+ a 1))
宏对其参数var不进行eval,所以翻译后的结果就是(setq a (+ a 1))
如果要定义一个可以接受增加多少的参数的incf,需要用到关键字参数&optional
(defmacro simple-incf (var &optional (amount 1)) (list 'setq var (list '+ var amount)))
只有一个参数即要被增加的变量的时候,缺省增加amount为1
为什么这里要使用macro?现在尝试定义一个做incf的函数,使用defun
(defun faulty-incf (var) (setq var (+ var 1))) (setf a 7) > (faulty-incf a) 8 > (faulty-incf a) 8 > a 7
可以发现函数调用之后,a的值还是7而没有改变,那是因为函数接受参数a的时候,本地实例化了一个变量var 作为拷贝,相当于call by value,所以不能改变a的值
setq
函数可以修改参数的值,但它不是一个macro,它是一种special function
14.5 macros as syntactic extensions
普通函数和宏函数有三个重要的区别:
- 普通函数的参数都会eval,而宏函数的参数不会被eval
- 普通函数的结果可以是任意的值,而宏产生的结果一定要是合法的lisp表达式,因为翻译之后还要执行表达式
- 宏返回一个合法表达式之后,马上会对其进行eval
除此之外,lisp中还有一些特殊函数如setq, if, let, block等不属于普通函数,它们也不会对参数eval。通过普通 函数和特殊函数的组合使用,其实也可以完成任意使用macro实现的任务
14.6 backquote
backquote符号即`,类似与单引号的用法,也是为了阻止变量被eval,不同之处在于反引号对一个list使用时, 里面的元素可以在前面加上一个逗号,
,表示"unquoted",即要使用它的值而不是表达式本身
(setf name 'fred) > `(this is ,name) (this is fred) > `(i give ,name ,(* 10 10) dollars) (i give fred 100 dollars)
ex 14.5
(defmacro set-mutual (a b) `(progn (setf ,a ',b) (setf ,b ',a))) (setf a 'hello) (setf b 'world) (set-mutual a b) > a b > b a
这个函数将a的值置为b的变量名,将b的值置为a的变量名,`(setf ,a ',b)中,a即引用a变量,不是a的值,这里可以 理解为a的变量名,然后',b表示先,b得到b的变量名再加单引号表示这个符号
14.7 splicing with backquote
上一节对反引号的list里面的元素使用逗号可以"unquote",即忽视反引号对其eval。 ,@
的用法类似逗号,作用是对该元素eval,并且得到的结果要是一个list,然后将list里面的全部元素拿出来 替换原来的位置,即不要list的括号
(setf name 'fred) (setf address '(10 maple drive)) > `(,name lives at ,address) (fred lives at (10 maple drive)) ;;;不要地址两边的括号 > `(,name lives at ,@address now) (fred lives at 10 maple drive)
通过&rest
参数可以搜集主体的表达式列表,来定义这样一个宏,接着使用comma-at来扒开这个列表并执行里面的 语句
(defmacro while (test &rest body) `(do () ((not ,test)) ,@body))
有了这个while宏就可以实现一个快速排序的程序quicksort,这是一个非常依赖宏的程序,输入为一个vector,还有 排序区域的左右下标l,r
(defun quicksort (vec l r) (let ((i l) (j r) ([ (svref vec (round (+ l r) 2)))) (while (<= i j) (while (< (svref vec i) p) (incf i)) (while (> (svref vec j) p) (decf j)) (when (<= i j) (rotatef (svref vec i) (svref vec j)) (incf i) (decf j))) (if (>= (- j l) 1) (quicksort vec l j)) (if (>= (- r i) 1) (quicksort vec i r))) vec)
程序说明:
- 每次选取主键是取中间那个数作为主键,(round (+ l r) 2)算出中间位置下标
- 下标i,j从两边开始向中间收缩,保证i左边的数都小于主键,右边的数都大于主键,而[i,j]之间的数则待处理
- 每次准备交换之前,i位置的数>=主键,j位置的数<=主键,交换两个位置的数就可以继续满足上一条件
- 结束时将原区域划分为主键那个数的左右两边两个区域,多余一个数的区域则继续递归调用该函数来排序
设计宏
设计一个宏ntimes,接受一个数字n并且对主体求值n次 比如(ntimes 10 (princ ".")) -> .........
下面是一个不正确的定义
(defmacro ntimes (n &rest body) `(do ((x 0 (+ x 1))) ((>= x ,n)) ,@body))
下面定义的宏函数set-zero接收一系列的参数并将它们置为0,并返回操作的信息,即翻译后的结果为
> (ppmx (set-zero a b c)) (progn (setf a 0) (setf b 0) (setf c 0) '(zeroed a b c))
现在要拼接一系列的(setf a 0) ... ,可以考虑对参数list使用mapcar,对每个元素返回一个(setf a 0)这样的 list,然后因为mapcar会将这些list再组成一个list返回,所以可以用,@来将外层的括号去掉,成为一系列 可以用progn执行的语句
(defmacro set-zero (&rest vars) `(progn ,@(mapcar #'(lambda (var) `(setf ,var 0)) vars) '(zeroed ,@vars)))
代码中的引号可能会感觉有点奇怪,最外面一层是反引号,而最后'(zeroed ,@vars)
则用单引号就行, 可能是最外面一层的反引号对这里仍然起作用,如果将这个单引号改为反引号则会提示变量vars没有值的错误。 而中间lambda函数中(setf)外面用的则是反引号
ex 14.6
(defmacro variable-chain (&rest vars) `(progn ,@(do ((v vars (rest v)) (res nil)) ((null (rest v)) (reverse res)) (push `(setf ,(first v) ',(second v)) res))))
14.8 complier
编译器可以将lisp程序编译为机器语言。这样相比直接用解释器来运行程序可能速度要快10倍以上。 compile
可以编译一个函数,compile-file
则可以编译整个文件
(defun tedious-sqrt (n) (dotimes (i n) (if (> (* i i) n) (return i)))) > (compile 'tedious-sqrt) tedious-sqrt
compile加上'func-name就可以编译函数,后面调用这个函数速度将会变快
14.9 compilation and macro expansion
common lisp标准允许宏调用在任何时候被进行扩展,所以我们不应该写出那种有副作用的宏,比如赋值和i/o。 但是如果是宏扩展之后变成有副作用的表达式则没有问题
(defmacro bad-announce-macro () (format t "~&hello")) (defun say-hi () (bad-announce-macro)) > (compile 'say-hi) hello say-hi > say-hi nil
这个例子中宏在编译say-hi函数的时候进行了扩展,所以编译的时候已经输出hello,剩下结果是nil,所以后面 调用函数只是输出nil,改进的方法是使宏返回一个format的表达式
(defmacro good-announce-macro () `(format t "~&hello"))
14.11 FSM
(defstruct (node (:print-function print-node)) (name nil) (inputs nil) (outputs nil)) (defun print-node (node stream depth) (format stream "#<Node ~A>" (node-name node))) (defstruct (arc (:print-function print-arc)) (from nil) (to nil) (label nil) (action nil)) (defun print-arc (arc stream depth) (format stream "#<ARC ~A / ~A / ~A>" (node-name (arc-from arc)) (arc-label arc) (node-name (arc-to arc)))) (defvar *nodes*) (defvar *arcs*) (defvar *current-node*) (defun initialize () (setf *nodes* nil) (setf *arcs* nil) (setf *current-node* nil)) (defmacro defnode (name) `(add-node ',name)) (defun add-node (name) (let ((new-node (make-node :name name))) (setf *nodes* (nconc *nodes* (list new-node))) new-node)) (defun find-node (name) (or (find name *nodes* :key #'node-name) (error "no node named ~A exists." name))) (defun add-arc (from-name label to-name action) (let* ((from (find-node from-name)) (to (find-node to-name)) (new-arc (make-arc :from from :label label :to to :action action))) (setf *arcs* (nconc *arcs* (list new-arc))) (setf (node-outputs from) (nconc (node-outputs from) (list new-arc))) (setf (node-inputs to) (nconc (node-inputs to) (list new-arc))) new-arc)) (defmacro defarc (from label to &optional action) `(add-arc ',from ',label ',to ',action)) (defun fsm (&optional (starting-point 'start)) (setf *current-node* (find-node starting-point)) (do () ((null (node-outputs *current-node*))) (one-transition))) (defun one-transition () (format t "~&state ~A. input: " (node-name *current-node*)) (let* ((ans (read)) (arc (find ans (node-outputs *current-node*) :key #'arc-label))) (unless arc (format t "~&no arc from ~A has label ~A.~%" (node-name *current-node*) ans) (return-from one-transition nil)) (let ((new (arc-to arc))) (format t "~&~A" (arc-action arc)) (setf *current-node* new)))) (initialize) (defnode start) (defnode have-5) (defnode have-10) (defnode have-15) (defnode have-20) (defnode end) (defarc start nickel have-5 "clunk!") (defarc start dime have-10 "clink!") (defarc start coin-return start "nothing to return!") (defarc have-5 nickel have-10 "Clunk!") (defarc have-5 dime have-15 "Clink!") (defarc have-5 coin-return start "Returned five cents.") (defarc have-10 nickel have-15 "Clunk!") (defarc have-10 dime have-20 "Clink!") (defarc have-10 coint-return start "Returned ten cents.") (defarc have-15 nickel have-20 "Clunk!") (defarc have-15 dime have-20 "Nickel change.") (defarc have-15 gum-button end "Deliver gum.") (defarc have-15 coin-return start "Returned fifteen cents.") (defarc have-20 nickel have-20 "Nickel returned.") (defarc have-20 dime have-20 "Dime returned.") (defarc have-20 gum-button end "Deliver gum, nickel change.") (defarc have-20 mint-button end "Deliver mints.") (defarc have-20 coin-return start "Returned twenty cents.")
ex 14.11
(defun compile-arc (arc) `((equal this-input ',(arc-label arc)) (format t "~&~A" ,(arc-action arc)) (,(node-name (arc-to arc)) (rest input-syms)))) (defun compile-node (node) `(defun ,(node-name node) (input-syms &aux (this-input (first input-syms))) (cond ((null input-syms) ',(node-name node)) ,@(mapcar #'compile-arc (node-outputs node)) (t (error "no arc from ~A with label ~A." ',(node-name node) this-input))))) (defmacro compile-machine () `(progn ,@(mapcar #'compile-node *nodes*)))
14.12 &body
使用宏的原因是可以给lisp增加一些新的语法,如实现一个while循环
(defmacro while (test &body body) `(do () ((not ,test)) ,@body))
这里&body类似于&rest的用法,但是lisp为了表示一些控制结构的主体还有可读性提供了&body关键词。
14.14 macros and lexical scoping
看回之前的函数faulty-incf,希望使用函数而不是宏来实现incf。如果我们在调用函数的时候不是 (faulty-incf a), 而是通过(faulty-incf 'a),在a前面加上单引号。这样函数就要找出参数当前的值并 用新的值替代它
如果参数是全局变量这时可以实现的。我们可以使用symbol-value
来获取符号的变量值,然后通过set
来将 新的值存到这个符号(全局变量)的变量值的空间,即真正修改全局变量的值
(defun faulty-incf (var) (set var (+ (symbol-value var) 1))) (setf a 9) > (faulty-incf 'a) 10 > a 10
这样就可以在函数中修改全局变量的值。注意新的faulty-incf在调用的时候要在变量名前面加上单引号,作为一个 symbol来传到函数中。否则会因为没有这个symbol而报错
faulty-incf只能对全局变量使用,而局部变量就会出错。假设在一个函数中对它进行调用
(defun test-faulty (turnip) (faulty-incf 'turnip)) (defun test-simple (turnip) (simple-incf turnip))
在正确的使用宏的test-simple中,首先会创建一个本地变量turnip,然后对其进行incf。而test-faulty会先创建 变量turnip,然后调用simple-incf,进入后创建本地变量var = 'turnip,然后对其加1会出错。而我们原先 希望执行的是(symbol-value 'turnip) -> value of 'turnip,而不是(symbol-value var) - > 'turnip
14.15 dynamic scoping
前面我们使用过的作用域都是lexical scoping,一个函数只能访问到在这个函数里面说明的变量,或者全局变量。
另一种方法是使用dynamic scoping.所谓动态,就是说一个变量名不一定总是绑定一个全局变量,可以在一个 函数里面使用同样的变量名,这时相当于覆盖掉全局的这个变量名,所有访问这个变量名都会访问到这个新的 变量,直到这个函数结束
动态作用域的变量也称为特殊变量。当一个变量声明为特殊变量的时候,它不是任何函数的局部变量。
defvar
宏可以声明一个特殊变量
(defvar birds) (setf fish '(hello world)) (setf birds '(a bird)) (defun ref-rish () fish) (defun ref-birds () birds) (defun test-lexical (fish) (list fish (ref-fish))) > (test-lexical '(new fish)) ((new fish) (hello world))
test函数中先创建局部变量fish,所以list中第一个元素是新的fish.但是调用ref-fish时,它只能访问到全局变量的 fish
(defun test-dynamic (birds) (list birds (ref-birds))) > (test-dynamic '(new bird)) ((new bird) (new bird)) > (ref-bird) (a bird)
进入test函数会创建一个新的动态变量birds,然后这时任何函数访问birds都会得到这个新的birds,直到test结束
14.17 defvar, defparameter, defconstant
三个函数都用于声明特殊变量,都有同样的形势如(func var-name value doc-string).
> (defvar *total-glassed* 0 "total glasses sold so far") *total*glasses*
如果变量本身已经有一个值,defvar中给的值不会改变变量本身的值,除非变量本身没有值defvar才会给它赋值
defparameter类似于defvar,用来声明一些程序运行时不会改变的变量,不同的是它会修改变量的值,即使变量 本身已经有一个值
defconstant用来声明常量,一旦声明之后不能对该变量的值进行修改,否则会出错