宏(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