宏(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
4. LOOP 宏
;;; LOOP 宏的循环表达式被设计成类似英文语法,所以多数关键字有一些同义词,在不同的语境下可以更接近英语。作为 Common Lisp 独特特性之一的 LOOP,本身非常复杂,理解它的最好办法就是不断练习。 ;;; from..to,递增循环 n 次,类似其他语言中的 for 循环: (loop for i from 1 to 3 do (print i)) ;; 输出: ;; 1 ;; 2 ;; 3 ;; 以下示例,以 i 作为计数器,一直循环到 i=n 才结束,总共循环了 3 次,指定 below 关键字可以让它循环 n-1 次: (loop for i from 1 below 3 do (print i)) ;; 输出: ;; 1 ;; 2 ;;; by 关键字指定步长: (loop for i from 1 to 10 by 2 do (print i)) ;; 输出: ;; 1 ;; 3 ;; 5 ;; 7 ;; 9 ;;; from..above,递减循环 n 次: (loop for i from 10 above 0 do (print i)) ;; 输出: ;; 10 ;; 9 ;; 8 ;; 7 ;; 6 ;; 5 ;; 4 ;; 3 ;; 2 ;; 1 ;;; in,遍历一个 list: (loop for i in '(1 2 3 4 5) do (print i)) ;; 输出: ;; 1 ;; 2 ;; 3 ;; 4 ;; 5 ;;; 当然,如果是需要对每个元素做一些操作,推荐用和 map 相关的函数,如上的例子可用 mapcar 代替: (mapcar #'print '(1 2 3 4 5)) ;; 输出: ;; 1 ;; 2 ;; 3 ;; 4 ;; 5 ;;; 遍历 association list(关联表): ;; for..in 也支持对关联表的遍历: (loop for (k v) in '((:a 1) (:b 2) (:c 3)) do (format t "~S: ~S" k v)) ; => :A: 1:B: 2:C: 3 (loop for (k . v) in '((:a 1) (:b 2) (:c 3)) do (format t "~S: ~S" k v)) ; => :A: (1):B: (2):C: (3) (loop for (k . v) in '((:a . 1) (:b . 2) (:c . 3)) do (format t "~S: ~S" k v)) ; => :A: 1:B: 2:C: 3 ;;; on,cdr 方式递归列表: ;; 使用 on 关键字,每次都会返回列表的 cdr: (loop for i on '(1 2 3 4 5) do (print i)) ;; 输出: ;; (1 2 3 4 5) ;; (2 3 4 5) ;; (3 4 5) ;; (4 5) ;; (5) ;; across,迭代向量(vector): (loop for i across #(1 2 3 4 5) do (print i)) ;; 输出: ;; 1 ;; 2 ;; 3 ;; 4 ;; 5 ;; 注意,字符串是由字符组成的向量,所以也可以用 across 遍历: (loop for i across "hello" do (print i)) ;; 输出: ;; #\h ;; #\e ;; #\l ;; #\l ;; #\o ;; hash-keys,按键迭代 hash 表: (loop for k being the hash-keys in h do (print k)) ; h = (:a 1 :b 2 :c 3) ;; 输出: ;; :A ;; :B ;; :C ;; 可以取值: (loop for k being the hash-keys in h using (hash-value v) do (print v)) ;; 输出: ;; 1 ;; 2 ;; 3 ;;; hash-values,按值迭代 hash 表: (loop for v being the hash-values in h do (print v)) ;; 输出: ;; 1 ;; 2 ;; 3 ;; 同样,也可以取键: (loop for v being the hash-values in h using (hash-key k) do (print k)) ;; 输出: ;; :A ;; :B ;; :C ;;; with,指定循环的初始变量: (loop with l = '(1 2 3) for i in l do (print i)) ;; 输出: ;; 1 ;; 2 ;; 3 ;; 请注意,“=”在这里是关键字,所以左右必须有空格,不能写成 l='(1 2 3) ;; 还可以指定子变量: (loop for i from 1 to 3 for x = (* i i) do (print x)) ;; 输出: ;; 1 ;; 4 ;; 9 ;;; when,条件判断: (loop for i from 1 to 10 when (evenp i) do (print i)) ;; 输出: ;; 2 ;; 4 ;; 6 ;; 8 ;; 10 ;;; while 和 until,循环终止条件: ;; while 直到满足条件后才执行 do 后面的表达式,并终止循环: (loop for i from 1 to 10 while (oddp i) do (print i)) ; => 1 ;; until 在没有满足条件之前会一直执行 do 后面的表达式: (loop for i from 1 to 10 until (> i 5) do (print i)) ;; 输出: ;; 1 ;; 2 ;; 3 ;; 4 ;; 5 ;;; collect,循环构造列表,每次会把 collect 后面的表达式的指放入一个列表中: (loop for i from 1 to 10 collect i) ; => (1 2 3 4 5 6 7 8 9 10) ;;; append,连接列表: (loop for i from 1 to 10 append (list i)) ; => (1 2 3 4 5 6 7 8 9 10) ;;; count,统计出循环过程中满足后面表达式的次数: (loop for i from 1 to 10 count (oddp i)) ; => 5 ;;; sum,汇总: (loop for i from 1 to 10 sum i) ; => 55 ;;; maximize,求最大值: (loop for i from 1 to 10 maximize i) ; => 10 (loop for i from 1 to 10 maximize (* i 2)) ; => 20 ;;; minimize,求最小值: (loop for i from 1 to 10 minimize i) ; => 1 (loop for i from 1 to 10 minimize (* i 2)) ; => 2