宏(macro)
Table of Contents
宏的本质是函数,它可以把一个表达式转换成另一个表达式,但宏不能被当作函数调用的,也就是说不能用 apply、funcall 等。
1. 宏示例1,实现 Python 的 range 函数
1.1. 在 Python 中的用法
range 是 Python 的一个内置函数,用来生成数字列表。以下是 range 在 Python 中的两种形式:
- range(stop)
- range(start, stop[, step])
当 range 只给一个参数 n 的时候,它会生成一个包含 0 到 n-1 连续数字的列表:
range(5) # [0, 1, 2, 3, 4]
当给定两个参数 n 和 m 时,参数 n 作为起点,生成从 n 到 m-1 连续数字的列表:
range(5, 10) # [5, 6, 7, 8, 9]
当给定第三个参数时,第三个参数作为步长:
range(1, 10, 2) # [1, 3, 5, 7, 9]
1.2. 使用 Common Lisp 实现
在 Common Lisp 中可以用 loop 来实现。
1.2.1. 第一步,实现 range(stop) 版本
我们定义一个函数叫 range,接受参数 stop,生成一个 0 到 stop-1 的连续数字列表,代码如下:
(defun range (stop) (loop for i from 0 below stop collect i))
然后在 REPL 中测试:
(range 10) ; => (0 1 2 3 4 5 6 7 8 9)
嗯…我们需要再实现一个指定范围的。
1.2.2. 第二步,实现 range(start, stop) 版本
从第一步实现的代码来看,我们只需要做一个小小改动,把 loop 里 from 关键字后面的值改成 start 即可:
(defun range (start stop) (loop for i from start below stop collect i))
然后测试下:
(range 1 5) ; => (1 2 3 4)
好了,最后需要再实现一个能指定步长的版本。
1.2.3. 第三步,让 range 可以指定步长
loop 的 by 关键字就可以指定步长,所以我们的代码改动仍然不大:
(defun range (start stop step) (loop for i from start below stop by step collect i))
在 REPL 中测试下:
(range 1 10 2) ; => (1 3 5 7 9)
1.2.4. 让 range 变得更方便
我们最终的代码是这样:
(defun range (start stop step) (loop for i from start below stop by step collect i))
如果要生成 0~9 的列表,在 Python 中调用 range(10) 即可,而我们的函数却要这样调用:(range 0 10 1)。看上去很不方便。
如果要实现 Python 那样方便的调用,我们得重新实现它。想想它的规律:如果只给参数 n,它生成 0~(n-1) 的列表;如果给两个参数 n 和 m,它生成 n~(m-1) 的列表;如果给定三个参数,最后个参数是它的步长。在这里参数一随着调用形式不同而有着不同的意义:如果只有一个参数,它代表着终止;如果是两个或三个参数,它表示起始。
所以,当调用 (range 10) 的时候,我们希望表达式是:
(loop for i from 0 below 10 by 1 collect i)
当调用 (range 5 10) 时,期望的表达式是:
(loop for i from 5 below 10 by 1 collect i)
为了生成这样的表达式,我们可以借助 Lisp 的宏,最终定义的 range 宏如下:
(defmacro range (&optional (start 0) (end nil) (step 1)) (loop for i from ,(if (null end) 0 start) below ,(if (null end) start end) by ,step collect i))
默认将 end 参数设为 nil,当它是 nil 时,start 表示的不是起始,而是结束,所以在代码块中使用了 if 来做判断,这两句 if 代码会在展开成最终表达式之前被执行。
看看我们执行 (range 10) 的时候,宏展开的样子:
(macroexpand-1 '(range 10)) ;; => ;; (LOOP FOR I FROM 0 BELOW 10 BY 1 ;; COLLECT I)
下面是执行 (range 5 10) 时展开的样子:
(macroexpand-1 '(range 5 10)) ;; => ;; (LOOP FOR I FROM 5 BELOW 10 BY 1 ;; COLLECT I)
2. 宏示例2,兼容多个 Common Lisp 实现
Common Lisp 虽然有语言标准,但语言标准覆盖范围并不广泛,超出标准部分的就要看具体的 Common Lisp 是如何实现了。好在 Common Lisp 标准定义了全局变量 *features*,里面保存每种实现的一些特性,以下是一些 CL 实现的执行结果:
Clisp:
[16]> *features* (:READLINE :REGEXP :SYSCALLS :I18N :LOOP :COMPILER :CLOS :MOP :CLISP :ANSI-CL :COMMON-LISP :LISP=CL :INTERPRETER :SOCKETS :GENERIC-STREAMS :LOGICAL-PATHNAMES :SCREEN :FFI :GETTEXT :UNICODE :BASE-CHAR=CHARACTER :WORD-SIZE=64 :PC386 :UNIX)
MKCL:
> *features* (:RELATIVE-PACKAGE-NAMES :UNICODE :LINUX :UNIX :IEEE-FLOATING-POINT :LITTLE-ENDIAN :X86-64 :ANSI-CL :COMMON-LISP :COMMON :MKCL)
SBCL:
* *features* (:QUICKLISP :SB-BSD-SOCKETS-ADDRINFO :ASDF2 :ASDF :ASDF-UNICODE :ALIEN-CALLBACKS :ANSI-CL :C-STACK-IS-CONTROL-STACK :COMMON-LISP :COMPARE-AND-SWAP-VOPS :COMPLEX-FLOAT-VOPS :CYCLE-COUNTER :ELF :FLOAT-EQL-VOPS :GENCGC :IEEE-FLOATING-POINT :INLINE-CONSTANTS :LARGEFILE :LINKAGE-TABLE :LINUX :LITTLE-ENDIAN :MEMORY-BARRIER-VOPS :MULTIPLY-HIGH-VOPS :OS-PROVIDES-BLKSIZE-T :OS-PROVIDES-DLADDR :OS-PROVIDES-DLOPEN :OS-PROVIDES-GETPROTOBY-R :OS-PROVIDES-POLL :OS-PROVIDES-PUTWC :OS-PROVIDES-SUSECONDS-T :RAW-INSTANCE-INIT-VOPS :SB-CORE-COMPRESSION :SB-DOC :SB-EVAL :SB-FUTEX :SB-LDB :SB-PACKAGE-LOCKS :SB-SOURCE-LOCATIONS :SB-TEST :SB-THREAD :SB-UNICODE :SBCL :STACK-ALLOCATABLE-CLOSURES :STACK-ALLOCATABLE-FIXED-OBJECTS :STACK-ALLOCATABLE-LISTS :STACK-ALLOCATABLE-VECTORS :STACK-GROWS-DOWNWARD-NOT-UPWARD :UNIX :UNWIND-TO-FRAME-AND-CALL-VOP :X86-64)
Clozure:
? *features* (:PRIMARY-CLASSES :COMMON-LISP :OPENMCL :CCL :CCL-1.2 :CCL-1.3 :CCL-1.4 :CCL-1.5 :CCL-1.6 :CCL-1.7 :CCL-1.8 :CCL-1.9 :CLOZURE :CLOZURE-COMMON-LISP :ANSI-CL :UNIX :OPENMCL-UNICODE-STRINGS :OPENMCL-NATIVE-THREADS :OPENMCL-PARTIAL-MOP :MCL-COMMON-MOP-SUBSET :OPENMCL-MOP-2 :OPENMCL-PRIVATE-HASH-TABLES :X86-64 :X86_64 :X86-TARGET :X86-HOST :X8664-TARGET :X8664-HOST :LINUX-HOST :LINUX-TARGET :LINUXX86-TARGET :LINUXX8664-TARGET :LINUXX8664-HOST :64-BIT-TARGET :64-BIT-HOST :LINUX :LITTLE-ENDIAN-TARGET :LITTLE-ENDIAN-HOST)
由于每种实现有不同的地方,写 Lisp 代码时,考虑兼容问题就可以使用 #+ 和 #- 宏。这两个宏会去匹配 *features*
里,如果匹配到或者未匹配到,执行后面的表达式。
例:实现 command line 的兼容
(defun get-command-line () (or #+SBCL (cdr *posix-argv*) #+CLISP *args* 'not-supported))
3. 展开宏
展开宏主要是为了检查编写的宏是否正确,或者查看某个宏是如何实现的。
macroexpand-1:查看宏展开一层后的内容
macroexpand:查看某个宏完全展开后的模样
查看表达式里所有宏最终展开的模样:
1)、部分 Common Lisp 里提供了 macroexpand-all,如 SBCL:sb-cltl2:macroexpand-all
2)、如果是在 Slime 开发环境,用快捷键 C-c M-m 或者 M-x slime-macroexpand-all
示例,查看 with-open-file 展开宏:
(macroexpand-1 '(with-open-file (stream #p"/etc/hosts" :direction :input) (print (read-line stream)))) ;; => ;; (LET ((STREAM (OPEN #P"/etc/hosts" :DIRECTION :INPUT)) (#:G660 T)) ;; (UNWIND-PROTECT ;; (MULTIPLE-VALUE-PROG1 (PROGN (PRINT (READ-LINE STREAM))) ;; (SETQ #:G660 NIL)) ;; (WHEN STREAM (CLOSE STREAM :ABORT #:G660)))) ;; T