宏(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中测试:

CL-USER> (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))

然后测试下:

CL-USER> (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中测试下:

CL-USER> (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)的时候,宏展开的样子:

CL-USER> (macroexpand-1 '(range 10))
(LOOP FOR I FROM 0 BELOW 10 BY 1
      COLLECT I)

下面是执行(range 5 10)时展开的样子:

CL-USER> (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展开宏:

CL-USER> (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循环

CL-USER> (loop for i from 1 to 3 do (print i))
1
2
3

注意上面例子的形式是:

for i to n

意思是i作为计数器,一直循环到i=n才结束,总共循环了3次,指定below关键字可以让它循环n-1次:

CL-USER> (loop for i from 1 below 3 do (print i))
1
2

还可以使用by关键字指定步长:

CL-USER> (loop for i from 1 to 10 by 2 do (print i))
1
3
5
7
9

from..above,递减循环n次:

CL-USER> (loop for i from 10 above 0 do (print i))
10
9
8
7
6
5
4
3
2
1

in,遍历一个List:

CL-USER> (loop for i in '(1 2 3 4 5) do (print i))
1
2
3
4
5

当然,如果是需要对每个元素做一些操作,推荐用和map相关的函数,如上的例子可用mapcar代替:

CL-USER> (mapcar #'print '(1 2 3 4 5))
1
2
3
4
5

遍历Association List(关联表):

for..in也支持对关联表的遍历:

CL-USER> (loop for (k v) in '((:a 1) (:b 2) (:c 3)) do (format t "~S: ~S" k v))
:A: 1:B: 2:C: 3
CL-USER> (loop for (k . v) in '((:a 1) (:b 2) (:c 3)) do (format t "~S: ~S" k v))
:A: (1):B: (2):C: (3)
CL-USER> (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:

CL-USER> (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):

CL-USER> (loop for i across #(1 2 3 4 5) do (print i))
1
2
3
4
5

注意,字符串是由字符组成的向量,所以也可以用across遍历:

CL-USER> (loop for i across "hello" do (print i))
#\h
#\e
#\l
#\l
#\o

hash-keys,按键迭代hash表:

; h = (:a 1 :b 2 :c 3)
CL-USER> (loop for k being the hash-keys in h do (print k))
:A
:B
:C

可以取值:

CL-USER> (loop for k being the hash-keys in h using (hash-value v) do (print v))
1
2
3

hash-values,按值迭代hash表:

CL-USER> (loop for v being the hash-values in h  do (print v))
1
2
3

同样,也可以取键:

CL-USER> (loop for v being the hash-values in h  using (hash-key k) do (print k))
:A
:B
:C

with,指定循环的初始变量:

CL-USER> (loop with l = '(1 2 3) for i in l do (print i))
1
2
3

请注意,“=”在这里是关键字,所以左右必须有空格,不能写成l='(1 2 3)

还可以指定子变量:

CL-USER> (loop for i from 1 to 3 for x = (* i i) do (print x))
1
4
9

when,条件判断:

CL-USER> (loop for i from 1 to 10 when (evenp i) do (print i))
2
4
6
8
10

while和until,循环终止条件:

使用while关键字,直到满足条件后才执行do后面的表达式,并终止循环:

CL-USER> (loop for i from 1 to 10 while (oddp i) do (print i))
1

另一个关键字until,在没有满足条件之前会一直执行do后面的表达式:

CL-USER> (loop for i from 1 to 10 until (> i 5) do (print i))
1
2
3
4
5

collect,循环构造列表:

指定collect关键字,每次会把collect后面的表达式的指放入一个列表中

CL-USER> (loop for i from 1 to 10 collect i)
(1 2 3 4 5 6 7 8 9 10)

append,连接列表:

CL-USER> (loop for i from 1 to 10 append (list i))
(1 2 3 4 5 6 7 8 9 10)

count,统计:

count关键字会统计出循环过程中满足后面表达式的次数

CL-USER> (loop for i from 1 to 10 count (oddp i))
5

sum,汇总:

CL-USER> (loop for i from 1 to 10 sum i)
55

maximize,求最大值:

CL-USER> (loop for i from 1 to 10 maximize i)
10
CL-USER> (loop for i from 1 to 10 maximize (* i 2))
20

minimize,最小值:

CL-USER> (loop for i from 1 to 10 minimize i)
1
CL-USER> (loop for i from 1 to 10 minimize (* i 2))
2