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