一般来说,凡是带有自定义的东东都会显得很专业,不过在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笔记》是一本深入探讨Common Lisp编程语言的实用教程。Common Lisp是一种功能强大的多范式编程语言,以其动态类型、宏系统和丰富的内置数据结构而闻名。这篇笔记详细记录了作者在学习过程中...
scratch少儿编程逻辑思维游戏源码-塔防游戏V1.0.zip
智慧城市-【精品】2019年中国智谷集团智慧城市解决方案.zip
# 压缩文件中包含: 中文-英文对照文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文-英文对照文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;
scratch少儿编程逻辑思维游戏源码-小不点的故事.zip
scratch少儿编程逻辑思维游戏源码-天空中的英雄.zip
# 压缩文件中包含: 中文文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;
GIS的组成和功能教育课件.ppt
# 压缩文件中包含: 中文文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;
scratch少儿编程逻辑思维游戏源码-速写图闯关.zip
# 压缩文件中包含: 中文-英文对照文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文-英文对照文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;
modbus_rt是一款完全基于C语言实现的可以运行在windows, linux, macos, RTOS上的modbus通信库。主要目的是针对控制系统和工业物联网的方向使用。modbus_rt可以同时支持slave模式和master模式,基于类似socket句柄的实现思想,可以很方便的支持多例模式。它不仅仅是modbus协议的解析和实现,而是尽可能的在应用层做好实现和封装,做到开箱即用,而不需要做过多的应用层移植。
# 压缩文件中包含: 中文文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;
包括:源程序工程文件、Proteus仿真工程文件、电路原理图文件、配套技术手册、论文资料等
# 压缩文件中包含: 中文-英文对照文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文-英文对照文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;
scratch少儿编程逻辑思维游戏源码-逃走.zip
# 压缩文件中包含: 中文-英文对照文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文-英文对照文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;
scratch少儿编程逻辑思维游戏源码-索尼克.zip
scratch少儿编程逻辑思维游戏源码-太空运输船.zip
# 压缩文件中包含: 中文文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;