clojure实战——何时使用宏
一、记住几点
- 在C语言中,宏在编译的时候会被文本替代,纯粹的文本替换。而在clojure中,宏在编译的时候会先被求值,然后求值后得到的数据结构代替宏原来的位置。而正是这个求值功能,使clojure的宏比C语言那种纯代码替代的宏具备更强大的功能。
- 不管是C语言的宏还是clojure的宏,它们的起作用的时期都是“编译期”。
- 如果可以最好不要用使用宏,优先使用函数。
二、为什么尽量避免使用宏?
最重要的原因:在clojure中,函数是头等公民,而宏不是。你无法在运行时候使用宏,也无法将一个宏作为参数传递给一个函数。宏虽然可用在函数式编程中,但宏不是函数式编程。另外,宏的代码相对于函数来说更难理解。
三、什么时候使用宏?
虽然原则上尽量不使用宏,但有一些事情是宏能做而函数不能做的,此时就需要用到宏。总体来说,包括下面三种情况:
1. 必须在编译时执行某些代码时
考虑这样一个需求:
在编译代码时,需要记录编译发生的时间。
要实现这种需求,像C语言这种宏是无法做到的,只能通过修改编译器才能做到,而使用clojure的宏,可以很简单实现这样的功能。比如可以定义如下宏:
(defmacro build-time []
(str (java.util.Date.)))
在编译的时候,这个宏会被求值,求值结果为:
"Thu Nov 23 21:40:33 CST 2017"
然后这个字符串将替代这个宏使用的位置。这就牛逼了!我们印象中编译时是不会执行代码的,而clojure在编译时,执行了str
这样的函数,因此,我们可以像写一个函数一样写一个宏。
另外,需要在编译的时候执行某些比较昂贵耗时的计算时,可以考虑使用宏进行优化。
2. 需要使用不被求值的参数时
在clojure中,经常使用宏写一些方便的语法结构(语法糖),以使代码更加优雅。此时,对于传入这些语法结构的参数,应该是不被求值的。比如clojure的when
:
(defmacro when
"Evaluates test. If logical true, evaluates body in an implicit do."
{:added "1.0"}
[test & body]
(list 'if test (cons 'do body)))
(macroexpand-1
'(when 1
2))
;; => (if 1 (do 2))
可以看到,when
其实是由if
实现的,其中body
不应该在test
检查之前被求值。在上述例子中,编译求值的时候,list
和cons
会被执行,求值后返回一个list数据结构
。
3. 需要使用内联代码
有时候函数调用代价比较高,或者是通过函数调用的形式不能达到使用宏替换的效果。比如,我们经常看到的日志输出:
在输出日志时,需要知道该日志输出所在的命名空间、在第几行输出的。
在上述需求场景中,如果我们定义一个日志输出的函数:
1 (ns my-log)
2
3 (defn log-info
4 [message]
5 (log message)
然后再其他命名空间调用该函数来输出日志,会发现所有日志都是由my-log
这个命名空间的第5行输出的,不满足要求。
此时,需要将log
需要被用作内联函数,使用宏就能很好地解决这个问题:
(ns log)
(defmacro log-info
[msg]
`(log message)
在其他命名空间使用该宏,编译时会代码替换,执行的时候不再是调用my-log
命名空间的函数了,而是在自己命名空间直接使用(log message)
。
四、总结
大概总结出三点我们可能会使用到宏的场景。当然,在实际程序设计和实现过程中,可以更加灵活地使用宏。但请记住一个原则:能用函数解决的问题,就不要用宏。另外,再记住一个宏的关键特性:宏只作用于编译期!