宏(macro)是 Lisp 语言中最重要的武器,它能够自动生成运行时的代码。宏也是编写领域特定语言(DSL)的利器,能够在不改动语言自己的基础上,增长新的程序构造体,这在其余语言中是不可能。好比,如今比较流行的同步方式写异步代码的 async/await,在非 Lisp 语言须要语言自己支持,可是在 Lisp 里面能够经过几个宏来解决,能够参考:core.async。html
With great power comes great responsibility.java
因为宏的强大,掌握编写它的方法有较大的难度,因此社区通常会建议能不用就不要用它,我我的也比较认同这一点,可是对于一些场景用宏确实也很方便,能使程序简洁明了,因此仍是有必要掌握它的,等有必定经验,就能够知道在什么场景下使用最合适了。git
为了由浅入深、系统地介绍宏,打算分两篇文章来介绍,第一篇为理论篇,主要介绍github
Symbol 数据类型,为何须要它,以及其余非 Lisp 语言为何没有express
宏的本质,宏运行时期编程
第二篇是实战篇,介绍宏的一些常见技巧以及一些通用宏。这两篇均以 Clojure 方言为示例,但其概念原理在其余 Lisp 中都是相通的。本文为第一篇。ruby
在 Clojure 里面,除了 number,string,list,map 等常见基本数据类型外,还有一个比较特殊的 Symbol 类型,定义以下:闭包
Symbols are identifiers that are normally used to refer to something else.异步
能够说 symbol 就是一些标识符,用来代指其余东西,和英文中 he/she/it 等「代词」的做用差很少。
如今编程使用的都是高级语言,其语言构造体(language's constructs)是由一些关键字与用户自定义变量组成,而这些都是 symbol,好比下面一个 Hello World 的示例:async
(defn hello [greeting] (println "Hello " greeting)) (hello "world")
上面的 defn, hello, greeting, println 都是 symbol,用来指向其余数据类型,在进行求值(eval)时,Compiler 会计算出相应值。Clojure 里面提供了 def 这个special form来创建 symbol 到其余值的映射关系。例如:
user> (def cat "Tom") #'user/cat
简单来讲上面这一句把 symbol 执行了字符串Tom
,可是因为 Clojure 里面为了实现其动态特性,真实的状况稍微复杂一些,见下图:
Var 是Clojure里面提供的四种引用类型中最经常使用的,支持动态做用域以及 thread-local 值。动态做用域是指函数内的自由变量的值是在运行时肯定的,这里不清楚的能够参考个人另外一篇文章《编程语言中的变量做用域与闭包》,这里不在赘述。
继续回到上面的图,def 会把 cat 这个 symbol 指向同名的 var,同时把 var 指向字符串 Tom(叫作 root binding)。symbol 到 var 的映射关系保存在每一个 namespace 中,能够用resolve
来查询这个映射关系:
user> (resolve 'cat) #'user/cat
最后一点须要注意的是,symbol 在 var 的映射关系只有在用 def 定义全局 var 是才具备,使用 let, loop 定义的词法做用域(lexical context)里面的 binding 不算是变量,一旦建立后没法修改。可使用 with-local-vars
宏来定义局部变量:
(with-local-vars [x 1 y 2] (var-set x 11) (+ (var-get x) (var-get y))) ;; => 13
由上面介绍咱们能够知道,Clojure 编译器在遇到 symbol 时,会对其进行求值,能够用'
来表示其字面量
user> (def tom 'tom) ;; 这里指向的值为一个 symbol 类型的 tom #'user/tom user> tom tom
有一点很是重要,任何两个同名的 symbol 是不相等的
user> (identical? 'tom 'tom) false
初次接触可能会有些疑惑,但也比较好理解,能够想一想
相同名的变量,在不一样地方,不一样时刻具备不一样的值是再正常不过的了。
并且 symbol 能够带有元数据,因此颇有可能同名的 symbol 具备不一样的元数据。相比之下,关键字(keyword)类型因为其值指向自身,相同名的关键字就是同一个对象,因此 keyword 是不容许带元数据的。
user> (identical? :tom :tom) true user> (with-meta :tom {:some-prop true}) ;; 会报错 java.lang.ClassCastException: clojure.lang.Keyword cannot be cast to clojure.lang.IObj
最后比较重要的一点,宏传入的参数都是 symbol 字面量,好比:
user> (def a 1) #'user/a user> (defmacro demo-macro [params] (println params)) #'user/demo-macro user> (demo-macro a) a nil
能够看到这里打印的是symbol a
,而不是数字 1。因为demo-macro
什么也没返回,因此在打印出 a 以后输出了 nil。
因为 Lisp 采用 s-expression 做为其语法,因此自然具备 code as data 的特色,也就是说 Lisp 程序自己就是一个 Lisp 数据,能够像操做其余数据类型同样来操做程序自己,这其实就是宏作的事情。老牌 Lisp hacker Paul Graham 在黑客与画家一书中有提到:
Lisp 并不严格区分读取期、编译器、运行期。在编译期去运行就是宏,能够用来扩展语言。
关于这三个时期的关系,能够用下面的图来表示
因为运行期主要进行的是函数的调用,结合上面 symbol 的知识能够这么定义宏
宏是编译期执行的函数,参数为 symbol 类型,返回由 symbol 组成的程序(也是数据)。返回值在运行时执行。
定义宏的defmacro
自己也是个宏,能够将其展开:
user> (macroexpand '(defmacro demo-macro [params] params)) (do (defn demo-macro ([&form &env params] params)) (. #'demo-macro (setMacro)) #'demo-macro)
能够看到,Clojure 里面是调用 setMacro
来将宏与通常的函数分开。
关于宏展开(通常不说宏调用)在整个 Clojure 程序生命周期中的位置,能够参考下图:
若是对实现细节感兴趣,能够参考我以前的文章:《Clojure 运行原理之编译器剖析篇》。
对于初学 Lisp 的同窗,对 symbol 类型不是很清楚,究其缘由是这个类型在非 Lisp 语言中是不存在的,为何不存在呢?主要是由于它们无法像 Lisp 同样具备 code as data 的特色,有些非 Lisp 语言可能也有所谓的 symbol 类型,像 ruby,可是 DSL 在 ruby 中与 Lisp 是彻底不同的。区别能够参考下图
What makes Lisp macros possible, is so far still unique to Lisp, perhaps because (a) it requires those parens, or something just as bad, and (b) if you add that final increment of power, you can no longer claim to have invented a new language, but only to have designed a new dialect of Lisp ; -)
Paul Graham 「What makes Lisp different」