盒子
盒子
文章目录
  1. lisp宏的魔力
    1. lisp的宏
    2. 宏展开
    3. 更复杂一些的宏
      1. 一个分割列表的宏
      2. 避免重复的执行
      3. 避免变量捕获
    4. 递归宏
    5. 宏的危险与其它选择
    6. 小结
    7. 最后的废话

Macro in Lisp

lisp宏的魔力

lisp的宏

注意:此宏远与cpp中的宏有着天壤之别,而且远复杂很多倍。
宏编程允许你创造自己的语言。当面对一个复杂编程挑战时,有经验的程序猿会问自己:那种语言适合解决这个问题?然后他用宏把lisp变成了那种语言。太扯了……

先举例说明什么是宏

看这个例子

1
(defun add (a b)
  (let ((x (+ a b)))
    (format t "The sum is ~a" x)
    x)) 
(add 3 4)

有没有感觉括号太多了?我们定义个let1宏

1
(defmacro let1 (var val &body body)
  `(let ((,var ,val))
     ,@body))

&body表示剩下的部分是表达式,而且隐含着一个progn的并行表达式,回忆”`”与”,”的作用”,@”相对于”,”用来表征并行表达式。

试试你的宏

1
(let ((foo (+ 2 3)))
  (* foo foo))
(let1 foo (+ 2 3)
  (* foo foo))
(defun add (a b)
  (let1 x (+ a b)
    (format t "The sum is ~a" x)
    x))

宏展开

宏的展开发生在运行期前,它把你非标准的程序转换成标准的lisp语言,然后传递给标准lisp解释器

你可以用macroexpand来展开宏

1
(macroexpand '(let1 foo (+ 2 3)
                (* foo foo)))

这将是很有效的调试方法。

更复杂一些的宏

回忆下之前我们定义的尾递归优化length函数

1
(defun my-length (lst)
  (labels ((f (lst acc)
             (if lst
               (f (cdr lst) (1+ acc))
               acc)))
    (f lst 0)))

正如所见,我们反复使用cdr,反复检查列表是否为空。我们用冗长的工作去创造局部函数f,我们可以用宏让它更简洁一些。

一个分割列表的宏

一个能把列表分开,并能把列表头和尾提供给我们用split宏非常有用。我们可以得到这么一个有bug的宏:

1
;Warning!Containing Bugs!
(defmacro split (val yes no)
  `(if ,val
     (let ((head (car ,val))
           (tail (cdr ,val)))
       ,yes)
     ,no))

列表的car和cdr分别保存在head和tail中。如果列表不能split,将不会创建head和tail变量。

于是my-length函数整洁些了

1
(defun my-length (lst)
  (labels ((f (lst acc)
             (split lst
                    (f tail (1+ acc))
                    acc)))
    (f lst 0)))

避免重复的执行

试试这样:

1
(split (progn (princ "Lisp rocks!")
              '(2 3))
       (format t "This can be split into ~a and ~a." head tail)
       (format t "This cannot be split."))

哦~为什么会执行三次打印Lisp rocks!展开宏看看

1
(macroexpand '(split (progn (princ "Lisp rocks!")
                            '(2 3))
                     (format t "THis can be split into ~a and ~a." head tail)
                     (format t "This cannot be split")))

你看到了什么……某动作被执行了三次。

我很惊异的是这种情况common lisp也接受,还可以这样把命令写在函数中。

1
(car (progn (princ "Lisp rocks!") '(2 3)))
Lisp rocks!
2

为了避免这种情况我们重新改写split宏,注意,还是有bug的版本

1
;Warning! Still contains a bug!
(defmacro split (val yes no) 
  `(let1 x ,val 
     (if x 
       (let ((head (car x))
             (tail (cdr x)))
         ,yes) 
         ,no)))

可以在宏内使用其它宏,像这里我们用了之前的let1宏。我们用一个变量绑定表达式,这时就不会重复执行了。但有了个新问题。

避免变量捕获

问题在哪里呢?试试下面的式子

1
(let1 x 100
  (split '(2 3)
         (+ x head)
         nil))

什么情况,竟然报错了!!我们展开它看看

1
(macroexpand '(split '(2 3)
                     (+ x head)
                     nil))

split意外地捕获了外部变量x并且覆盖了前者。怎么避免这个问题??
这个函数专门用来解决这类问题

1
(gensym)

它返回随机的名字,并且一定不会和已有的变量重复。使用它我们可以获得最终能安全使用的split版本

1
(defmacro split (val yes no)
  (let1 g (gensym)
    `(let1 ,g ,val
       (if ,g
         (let ((head (car ,g))
               (tail (cdr ,g)))
           ,yes)
         ,no))))

两次绑定,先随机生成一个变量名,在把具体的值绑在这个随机生成的变量名上。可以展开看看

1
;Each time is different
(macroexpand '(split '(2 3)
                     (+ x head)
                     nil))

每次结果都不同。

递归宏

再次考虑我们的my-length函数

1
(defun my-length (lst)
  (labels ((f (lst acc)
             (split lst
                    (f tail (1+ acc))
                    acc)))
    (f lst 0)))

局部函数f重复的建立。我们可以通过宏来简化它。想象这么一个recurse宏:

1
(recurse (n 9)
(fresh-line)
(if (zerop n)
(princ "lift-off!")
(progn (princ n)
(self (1- n)))))
9
8
7
6
5
4
3
2
1
lift-off!

为了将列表奇数项作为变量偶数项作为值,可以写一个函数来生成cons对。像这样

1
(pairs '(a b c d e f))
((A . B) (C . D) (E . F))

函数可以这样定义,看着真绕……

1
;tail-call-optimized list-eater
(defun pairs (lst)
  (labels ((f (lst acc)
             (split lst
                    (if tail
                      (f (cdr tail) (cons (cons head (car tail)) acc))
                      (reverse acc))
                    (reverse acc))))
    (f lst nil)))

然后可以写出recurse宏

1
(defmacro recurse (vars &body body)
  (let1 p (pairs vars)
    `(labels ((self ,(mapcar #'car p)
                ,@body))
       (self ,@(mapcar #'cdr p)))))

重新定义我们的my-length函数

1
(defun my-length (lst)
  (recurse (lst lst
                acc 0)
           (split lst
                  (f tail (1+ acc))
                  acc)))

作者费了一大圈后认为现在看着很简洁,你觉得呢?

宏的危险与其它选择

把语言定义成任意你想要的模式,这当然很危险。若干岁月后回头都看不懂你自己写的什么,何况别人!所以,能用函数式解决的问题就别用宏来解决吧,骚年。看看我们的my-length函数,可以用高阶函数reduce轻松简化。

1
(defun my-length (lst)
  (reduce (lambda (x i)
            (1+ x))
          lst
          :initial-value 0))

reduce可以连续调用上次结果并与当前列表项一同运算。i代表当前为第几项,对我们没什么用,最后x初值为0一定要加上。

然而仍然有些问题用函数式并不能很好解决而用宏可以轻易处理。下一章?拭目以待吧。

小结

  • 宏允许你用代码生成代码,通过宏,你可以创造自己的语言,并在lisp解释器解释之前转化成标准lisp。

  • 宏允许你摆脱一种既视感。摆脱各种括号和重复

  • 当你写宏的时候你必须小心,这样才不会导致意外的重复执行的代码

  • 你需要避免变量捕获,这可以通过gensym来解决

  • 如果一个宏创造的变量故意暴露,作为宏的一个特性,这种宏叫做anaphoric macros

  • 宏编程是非常有力的技术。然而,尽量使用函数式编程代替宏来解决问题。宏是最后的法宝。

最后的废话

今天难得去图书馆了,三楼看到本real world haskell,只能感慨自己没空看了,二楼空无一人,看着散乱而泛黄的旧书顿生悲凉。一楼人倒挺多,都是来为四六级托福雅思考研等等等等自习的,很少有看书的。三楼人也有些,不过都集中在文学小说和艺术的区域。

不知为什么,总有种世风日下的感觉。回忆起当初在图书馆看书的日子……