一般来说,凡是带有自定义的东东都会显得很专业,不过在lisp中,这才算刚刚开始。至于你信不信,反正我是信了。就让我们开始吧。
说来有些不可理喻,宏之所以难以理解,是因为它在lisp中太过自然(天生的阿),运用起来毫不费力。以至于很容易被误解为一个有意思的函数。事实上,虽然宏真的很像函数,但仅仅是像而已。他们完全不在一个层面上,抽象的层次也大不相同。
一旦理解了宏和函数的区别,恭喜你,晋级了!你将会进入一个全新的层次(这个初等教材已经不适合你了)。不过,既然你还在看,那就让我们继续吧。唉,作者在这里讲了一个故事,就不罗嗦了,这个故事教育我们:如果,你会写宏,那么纵然你一天到晚不呆办公室,你的活也能做完(宏帮你做了)~这就是个奇迹,而且已经发生了。
宏时间和运行时间
理解宏的关键在于可以分清楚宏和普通代码的区别。宏是用来生成普通代码的,而普通代码则是用来被解释、编译的。宏生成普通代码所用时间为宏时间,普通代码(包括宏生成的)编译执行时间称为运行时间。lisp程序运行的先后顺序就是:先执行所有的宏,然后编译执行代码。
比较明显的表现就是,宏时间内是访问不了运行时间内的任何数据:
(defun foo (x)
(when (> x 10) (print 'big)))
在宏时间内,when并不能得到x参数的值,剖析下when宏内部:
(defmacro when (condition &rest body)
`(if ,condition (progn ,@body)))
宏时间内(> x 10)和(print 'big)被分别作为condition和body参数传入的,自然是不认识x为何许东东了。只有在运行时间内才运行这个:
(if (> x 10) (progn (print 'big)))
简而言之,宏不直接做事,在宏时间内他只生成做事的代码,然后在运行时间内由他生成的代码去做事(有点绕)。
自定义宏
定义宏与定义函数蛮像:
(defmacro name (parameter*)
"Optional documentation string."
body-form*)
一般来说,写一个自定义宏要三步:
1.写一个简单的宏调用,然后对它扩展编码,反之亦可
2.按照之前的简单调用参数来写一个可以生成之前手写的扩展代码的宏
3.确保覆盖面,没有坏味道
下面来做一个例子,生成质数的宏
首先,写俩功能函数:
(defun primep (number)
(when (> number 1)
(loop for fac from 2 to (isqrt number) never (zerop (mod number fac)))))
(defun next-prime (number)
(loop for n from number when (primep n) return n))
现在,来构建咱们的宏,大概调用起来应该这个样子:
(do-primes (p 0 19)
(format t "~d " p))
打印出0-19之间的所有质数
在没有咱宏的情况下,代码可能是这个样子:
(do ((p (next-prime 0) (next-prime (1+ p))))
((> p 19))
(format t "~d " p))
作为第一个自定义宏,大概这个样子:
(defmacro do-primes (var-and-range &rest body)
(let ((var (first var-and-range))
(start (second var-and-range))
(end (third var-and-range)))
`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
((> ,var ,end))
,@body)))
稍微再改改:
(defmacro do-primes ((var start end) &body body)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
((> ,var ,end))
,@body))
注意,这两版的主要差距便在于参数的区别,如果对参数项还比较陌生的可以参看前文。这里简单涉及下&rest和&body,他们的语义是相同的,不过&body更多用于宏中,以便标识。在此还涉及到了反单引号(`):
Backquote Syntax | Equivalent List-Building Code | Result |
`(a (+ 1 2) c) | (list 'a '(+ 1 2) 'c) | (a (+ 1 2) c) |
`(a ,(+ 1 2) c) | (list 'a (+ 1 2) 'c) | (a 3 c) |
`(a (list 1 2) c) | (list 'a '(list 1 2) 'c) | (a (list 1 2) c) |
`(a ,(list 1 2) c) | (list 'a (list 1 2) 'c) | (a (1 2) c) |
`(a ,@(list 1 2) c) | (append (list 'a) (list 1 2) | (list 'c)) (a 1 2 c) |
或许只有比较才能看出这个符号到底多么神奇,如果没有它,代码就算这个样子:
(defmacro do-primes-a ((var start end) &body body)
(append '(do)
(list (list (list var
(list 'next-prime start)
(list 'next-prime (list '1+ var)))))
(list (list (list '> var end)))
body))
完成了自定义宏,检查/观察它也很方便, MACROEXPAND-1函数就能做到:
CL-USER> (macroexpand-1 '(do-primes (p 0 19) (format t "~d " p)))
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P))))
((> P 19))
(FORMAT T "~d " P))
T
下面得进入第三步了。
宏检测/bug修复(这一段本人看着有些晦涩,建议参阅原文)
首先,之前的代码有一个比较明显的问题,即参数值的限定。如果这样,可能就要悲剧了:
(do-primes (p 0 (random 100))
(format t "~d " p))
让我们看看代码会怎么样:
CL-USER> (macroexpand-1 '(do-primes (p 0 (random 100)) (format t "~d " p)))
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P))))
((> P (RANDOM 100)))
(FORMAT T "~d " P))
T
连判定值都是随机数,也就是说,这个循环什么时候停也是随机的了。显然这是不靠谱的,比较简单的解决方式就是初始化下基本数值:
(defmacro do-primes ((var start end) &body body)
`(do ((ending-value ,end)
(,var (next-prime ,start) (next-prime (1+ ,var))))
((> ,var ending-value))
,@body))
简单是简单,不幸的是,似乎解决麻烦本身也带来了麻烦,比较明显的就是end比start更早的进入了系统,似乎问题也不太大(个人感觉的确不大阿,这里就米有看懂).按照某些原则是不可以这样的,于是乎掉个顺序:
(defmacro do-primes ((var start end) &body body)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(ending-value ,end))
((> ,var ending-value))
,@body))
它带来的另一个麻烦就是变量ending-value了,作为一个隐藏的内部变量,或许会一不小心被外部的覆盖了,比如这样:
(do-primes (ending-value 0 10)
(print ending-value))
(let ((ending-value 0))
(do-primes (p 0 10)
(incf ending-value p))
ending-value)
这样的杯具实在是防不胜防阿~为了避免这个杯具,lisp为咱提供了GENSYM:
(defmacro do-primes ((var start end) &body body)
(let ((ending-value-name (gensym)))
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(,ending-value-name ,end))
((> ,var ,ending-value-name))
,@body)))
解析下上面的代码就变成了这样:
(do ((ending-value (next-prime 0) (next-prime (1+ ending-value)))
(#:g2141 10))
((> ending-value #:g2141))
(print ending-value))
(let ((ending-value 0))
(do ((p (next-prime 0) (next-prime (1+ p)))
(#:g2140 10))
((> p #:g2140))
(incf ending-value p))
ending-value)
虽然经验可以解决很多问题,不过最安全的方式莫过于此了。
通常来说,检验/修复bug也是有套路的:
1.非必要情况下,无论是主体还是子体都应该按既定顺序排列
2.非必要情况下,确保子体只创建一次变量,之后只能将这变量当常量使用
3.在宏内建立变量要使用gensym
(感觉1和2也很晦涩,求助ing...)
宏不单能生成普通的code,如果你愿意,也能用来生成宏,比如上文的let.同样以上文为例:
(defmacro do-primes ((var start end) &body body)
(with-gensyms (ending-value-name)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(,ending-value-name ,end))
((> ,var ,ending-value-name))
,@body)))
(defmacro with-gensyms ((&rest names) &body body)
`(let ,(loop for n in names collect `(,n (gensym)))
,@body))
ps:相较于标准宏,自定义宏麻烦了好多(创造总比继承要麻烦的).其实这一章节我本人并没有通透,特别是尾部的校验那块。本着印证的心态,决定不再停留(停在这里好久了),继续向前吧,或许在后面的章节中能回过味来~
(未完待续)
分享到:
相关推荐
common-lisp-the-language-second-edition.PDF
cad-lisp-3-表操作.LSP.lsp
Practical Common Lisp-1st-2005,官方排版,我为大部分章节加了二级书签
该资源包含了 17 章节,从基础的列表、特殊数据结构、控制流程、函数、输入与输出、符号、数字、宏、Common Lisp 对象系统、结构、速度、进阶议题到高级主题的推论、生成 HTML、对象等。 ANSI Common Lisp 是一种...
Common Lisp the Language, 2nd Edition经典,喜欢commonlisp的朋友们的少有资源
Provides practical advice for the construction of Common Lisp programs. Shows examples of how Common Lisp is best used. Illustrates and compares features of the most popular Common Lisp systems on ...
AutoLisp源文件--标注高程.LSP
practical common lisp.pdf 实用Common.Lisp编程 英文版
Autocad的课件——AutoLISP-Visual-LISP教程.ppt
这本《Practical Common Lisp》之所以号称Practical,正是因为这本书大量介绍Common Lisp在现实世界中的各种应用方式,算是第一本「入世传教」的Common Lisp著作。《Practical Common Lisp》是目前最畅销的Common ...
lisp解密程序-适用于早期的Lisp程序
计算多个数字之和、计算多条线段长度之和、插入墙高标注、查询多段线顶点坐标并绘制、自动生成页码、绘制示坡线、插入排水箭头 https://blog.csdn.net/qq_24141055/article/details/121446354
内含ANSI Common Lisp+On Lisp+实用Common Lisp编程,带书签
Practical Common Lisp 学习lisp的入门书籍
practical common lisp英文版mobi for kindle https://github.com/akosma/PracticalCommonLisp_ePub
这本《Practical Common Lisp》之所以号称Practical,正是因为这本书大量介绍Common Lisp在现实世界中的各种应用方式,算是第一本「入世传教」的Common Lisp著作。《Practical Common Lisp》是目前最畅销的Common ...
Apress原始PDF印刷版本,页面比较精细,适合打印了看。pdf大小约17M,用7z压缩后约为7M。
Common-Lisp-Actors, 通用Lisp的actor系统 这是一个简单且易于使用的Actor系统,在。设置需要波尔多螺纹。http://common-lisp.net/project/bordeaux-threads/ 2. 加载 actors.lisp 并开始使用它。 如果你有 Quick
If you’re interested in Lisp as it relates to Python or Perl, and want to learn through doing rather than watching, Practical Common Lisp is an excellent entry point. — Chris McAvoy, Chicago Python ...
NULL 博文链接:https://jamsa.iteye.com/blog/1188836