一般来说,凡是带有自定义的东东都会显得很专业,不过在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:相较于标准宏,自定义宏麻烦了好多(创造总比继承要麻烦的).其实这一章节我本人并没有通透,特别是尾部的校验那块。本着印证的心态,决定不再停留(停在这里好久了),继续向前吧,或许在后面的章节中能回过味来~
(未完待续)
分享到:
相关推荐
《Practical Common Lisp-1st-2005》是一本专注于Common Lisp编程语言的实用书籍,作者Peter Seibel通过这本书向读者展示了如何使用Common Lisp来解决真实世界中的问题,强调程序员作为工程师和艺术家的双重身份,而...
common-lisp-the-language-second-edition.PDF
cad-lisp-3-表操作.LSP.lsp
《Practical Common Lisp笔记》是一本深入探讨Common Lisp编程语言的实用教程。Common Lisp是一种功能强大的多范式编程语言,以其动态类型、宏系统和丰富的内置数据结构而闻名。这篇笔记详细记录了作者在学习过程中...
标题中的"emacs-lisp-intro-2.04.tar.gz"是一个典型的压缩文件名,它表明这个文件是一个关于Emacs Lisp的介绍性资料,并且版本号是2.04,格式为tar.gz。tar.gz是一种在Linux和Unix系统中常用的文件压缩格式,它先用...
Common Lisp的求值模型涉及表达式的求值、宏扩展和函数调用。等价性(Equivalence)是指在比较不同数据对象时使用的规则。错误处理(Errors)机制让程序在运行时能够响应各种异常情况。不可读数据对象(Unreadable ...
描述:“实用Common.Lisp编程.pdf,2011.10出版” 从这些信息中,我们可以提炼出几个关键的知识点: ### Common Lisp语言简介 Common Lisp是一种高级的、通用的、多范式的编程语言,它在Lisp家族中占据着重要的...
读者将了解到如何使用Common Lisp来处理数据结构,如树、图以及自定义的数据类型。同时,书中还会介绍与外部系统交互的方法,如文件I/O和网络编程。 Apress出版社的书籍以其专业性和实用性著称,此书也不例外。作者...
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
该资源包含了 17 章节,从基础的列表、特殊数据结构、控制流程、函数、输入与输出、符号、数字、宏、Common Lisp 对象系统、结构、速度、进阶议题到高级主题的推论、生成 HTML、对象等。 ANSI Common Lisp 是一种...
这本《Practical Common Lisp》之所以号称Practical,正是因为这本书大量介绍Common Lisp在现实世界中的各种应用方式,算是第一本「入世传教」的Common Lisp著作。《Practical Common Lisp》是目前最畅销的Common ...
标题中的“Lisp-music-player.rar”表明这是一个基于Lisp语言开发的音乐播放器软件,其源代码或可执行文件被压缩在RAR格式的文件中。RAR是一种流行的压缩格式,通常用于存储和分发多个文件,它允许用户将多个文件...
计算多个数字之和、计算多条线段长度之和、插入墙高标注、查询多段线顶点坐标并绘制、自动生成页码、绘制示坡线、插入排水箭头 https://blog.csdn.net/qq_24141055/article/details/121446354
### 实用Common Lisp编程知识点概览 #### 一、书籍简介与评价 《实用Common Lisp》是一本面向实际程序员的Lisp语言入门书籍。作者Peter Seibel通过一系列真实世界的问题来展示Common Lisp的强大之处,强调工程性和...
Practical Common Lisp 学习lisp的入门书籍
标题中的“pm.rar_cad_cad lisp_lisp_pm-c”暗示了这是一个关于CAD(计算机辅助设计)软件的编程资源,特别提到了LISP语言在CAD环境中的应用,以及可能与一个名为“PM”的特定功能或工具有关。描述中提到“CAD中软件...
2. **自定义宏**:更进一步地,书中还讲解了如何定义自己的宏来扩展Common Lisp的功能,使开发过程更加灵活高效。 #### 五、书籍资源与社区支持 1. **在线资源**:《Practical Common Lisp》提供了完整的在线版本...
Common-Lisp-Actors, 通用Lisp的actor系统 这是一个简单且易于使用的Actor系统,在。设置需要波尔多螺纹。http://common-lisp.net/project/bordeaux-threads/ 2. 加载 actors.lisp 并开始使用它。 如果你有 Quick