`
iyuan
  • 浏览: 464930 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

lisp初体验-Practical Common Lisp笔记-10.自定义宏

    博客分类:
  • lisp
阅读更多
一般来说,凡是带有自定义的东东都会显得很专业,不过在lisp中,这才算刚刚开始。至于你信不信,反正我是信了。就让我们开始吧。

说来有些不可理喻,宏之所以难以理解,是因为它在lisp中太过自然(天生的阿),运用起来毫不费力。以至于很容易被误解为一个有意思的函数。事实上,虽然宏真的很像函数,但仅仅是像而已。他们完全不在一个层面上,抽象的层次也大不相同。

一旦理解了宏和函数的区别,恭喜你,晋级了!你将会进入一个全新的层次(这个初等教材已经不适合你了)。不过,既然你还在看,那就让我们继续吧。唉,作者在这里讲了一个故事,就不罗嗦了,这个故事教育我们:如果,你会写宏,那么纵然你一天到晚不呆办公室,你的活也能做完(宏帮你做了)~这就是个奇迹,而且已经发生了。

宏时间和运行时间
理解宏的关键在于可以分清楚宏和普通代码的区别。宏是用来生成普通代码的,而普通代码则是用来被解释、编译的。宏生成普通代码所用时间为宏时间,普通代码(包括宏生成的)编译执行时间称为运行时间。lisp程序运行的先后顺序就是:先执行所有的宏,然后编译执行代码。
比较明显的表现就是,宏时间内是访问不了运行时间内的任何数据:
(defun foo (x)
  (when (> x 10) (print 'big)))

在宏时间内,when并不能得到x参数的值,剖析下when宏内部:
(defmacro when (condition &rest body)
  `(if ,condition (progn ,@body)))

宏时间内(> x 10)和(print 'big)被分别作为condition和body参数传入的,自然是不认识x为何许东东了。只有在运行时间内才运行这个:
(if (> x 10) (progn (print 'big)))

简而言之,宏不直接做事,在宏时间内他只生成做事的代码,然后在运行时间内由他生成的代码去做事(有点绕)。

自定义宏
定义宏与定义函数蛮像:
(defmacro name (parameter*)
  "Optional documentation string."
  body-form*)

一般来说,写一个自定义宏要三步:
1.写一个简单的宏调用,然后对它扩展编码,反之亦可
2.按照之前的简单调用参数来写一个可以生成之前手写的扩展代码的宏
3.确保覆盖面,没有坏味道

下面来做一个例子,生成质数的宏
首先,写俩功能函数:
(defun primep (number)
  (when (> number 1)
    (loop for fac from 2 to (isqrt number) never (zerop (mod number fac)))))

(defun next-prime (number)
  (loop for n from number when (primep n) return n))

现在,来构建咱们的宏,大概调用起来应该这个样子:
(do-primes (p 0 19)
  (format t "~d " p))

打印出0-19之间的所有质数
在没有咱宏的情况下,代码可能是这个样子:
(do ((p (next-prime 0) (next-prime (1+ p))))
    ((> p 19))
  (format t "~d " p))

作为第一个自定义宏,大概这个样子:
(defmacro do-primes (var-and-range &rest body)
  (let ((var (first var-and-range))
        (start (second var-and-range))
        (end (third var-and-range)))
    `(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
         ((> ,var ,end))
       ,@body)))

稍微再改改:
(defmacro do-primes ((var start end) &body body)
  `(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
       ((> ,var ,end))
     ,@body))

注意,这两版的主要差距便在于参数的区别,如果对参数项还比较陌生的可以参看前文。这里简单涉及下&rest和&body,他们的语义是相同的,不过&body更多用于宏中,以便标识。在此还涉及到了反单引号(`):
Backquote Syntax Equivalent List-Building Code Result
`(a (+ 1 2) c) (list 'a '(+ 1 2) 'c) (a (+ 1 2) c)
`(a ,(+ 1 2) c) (list 'a (+ 1 2) 'c) (a 3 c)
`(a (list 1 2) c) (list 'a '(list 1 2) 'c) (a (list 1 2) c)
`(a ,(list 1 2) c) (list 'a (list 1 2) 'c) (a (1 2) c)
`(a ,@(list 1 2) c) (append (list 'a) (list 1 2) (list 'c)) (a 1 2 c)


或许只有比较才能看出这个符号到底多么神奇,如果没有它,代码就算这个样子:
(defmacro do-primes-a ((var start end) &body body)
  (append '(do)
          (list  (list (list var
                             (list 'next-prime start)
                             (list 'next-prime (list '1+ var)))))
          (list (list (list '> var end)))
          body))

完成了自定义宏,检查/观察它也很方便, MACROEXPAND-1函数就能做到:
CL-USER> (macroexpand-1 '(do-primes (p 0 19) (format t "~d " p)))
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P))))
    ((> P 19))
  (FORMAT T "~d " P))
T

下面得进入第三步了。
宏检测/bug修复(这一段本人看着有些晦涩,建议参阅原文)
首先,之前的代码有一个比较明显的问题,即参数值的限定。如果这样,可能就要悲剧了:
(do-primes (p 0 (random 100))
  (format t "~d " p))

让我们看看代码会怎么样:
CL-USER> (macroexpand-1 '(do-primes (p 0 (random 100)) (format t "~d " p)))
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P))))
    ((> P (RANDOM 100)))
  (FORMAT T "~d " P))
T

连判定值都是随机数,也就是说,这个循环什么时候停也是随机的了。显然这是不靠谱的,比较简单的解决方式就是初始化下基本数值:
(defmacro do-primes ((var start end) &body body)
  `(do ((ending-value ,end)
        (,var (next-prime ,start) (next-prime (1+ ,var))))
       ((> ,var ending-value))
     ,@body))

简单是简单,不幸的是,似乎解决麻烦本身也带来了麻烦,比较明显的就是end比start更早的进入了系统,似乎问题也不太大(个人感觉的确不大阿,这里就米有看懂).按照某些原则是不可以这样的,于是乎掉个顺序:
(defmacro do-primes ((var start end) &body body)
  `(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
        (ending-value ,end))
       ((> ,var ending-value))
     ,@body))

它带来的另一个麻烦就是变量ending-value了,作为一个隐藏的内部变量,或许会一不小心被外部的覆盖了,比如这样:
(do-primes (ending-value 0 10)
  (print ending-value))

(let ((ending-value 0))
  (do-primes (p 0 10)
    (incf ending-value p))
  ending-value)

这样的杯具实在是防不胜防阿~为了避免这个杯具,lisp为咱提供了GENSYM:
(defmacro do-primes ((var start end) &body body)
  (let ((ending-value-name (gensym)))
    `(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
          (,ending-value-name ,end))
         ((> ,var ,ending-value-name))
       ,@body)))

解析下上面的代码就变成了这样:
(do ((ending-value (next-prime 0) (next-prime (1+ ending-value)))
     (#:g2141 10))
    ((> ending-value #:g2141))
  (print ending-value))

(let ((ending-value 0))
  (do ((p (next-prime 0) (next-prime (1+ p)))
       (#:g2140 10))
      ((> p #:g2140))
    (incf ending-value p))
  ending-value)

虽然经验可以解决很多问题,不过最安全的方式莫过于此了。
通常来说,检验/修复bug也是有套路的:
1.非必要情况下,无论是主体还是子体都应该按既定顺序排列
2.非必要情况下,确保子体只创建一次变量,之后只能将这变量当常量使用
3.在宏内建立变量要使用gensym
(感觉1和2也很晦涩,求助ing...)

宏不单能生成普通的code,如果你愿意,也能用来生成宏,比如上文的let.同样以上文为例:
(defmacro do-primes ((var start end) &body body)
  (with-gensyms (ending-value-name)
    `(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
          (,ending-value-name ,end))
         ((> ,var ,ending-value-name))
       ,@body)))

(defmacro with-gensyms ((&rest names) &body body)
  `(let ,(loop for n in names collect `(,n (gensym)))
     ,@body))


ps:相较于标准宏,自定义宏麻烦了好多(创造总比继承要麻烦的).其实这一章节我本人并没有通透,特别是尾部的校验那块。本着印证的心态,决定不再停留(停在这里好久了),继续向前吧,或许在后面的章节中能回过味来~

(未完待续)
3
1
分享到:
评论
3 楼 天国之翼 2011-09-14  
大耳冥冥
大眼窸窸
2 楼 yolio2003 2011-09-10  
待续啊 大哥 待续啊 继续啊
写的太好了 非常感谢
1 楼 lincpa 2011-08-11  
大善,期待下文!

相关推荐

Global site tag (gtag.js) - Google Analytics