带你了解Elixir的宏
介绍Lisp宏和Elixir宏的,以及通过示例代码,一步步为读者介绍Elixir的宏是如何在编译的过程中进行展开的
什么是宏
宏(Macro),是一种批量处理的称谓。一般说来,宏是一种规则或模式,或称语法替换 , 用于说明某一特定输入(通常是字符串)如何根据预定义的规则转换成对应的输出(通常也 是字符串)。这种替换在预编译时进行,称作宏展开。 说道宏,就不得不提一个经典语言和 它的宏。
Lisp的特点和它的宏
Lisp的特点:
- 数据就代码,代码就是数据
- LISP中所有的都是list, 当然也可以叫做S表达式。
- 如果把list中的第一元素视为函数,该list就可视作代码一样运行。术语叫做求值, evaluate。当然也可以不求值,此时list就是数据。因此这里引出一个重要概念 ,代码 也是数据,一切皆为数据,一切都是list。
Lisp的宏:
- 如果一个list传递给lisp函数,则先被求值为atom(一个特殊的list,不能再被求值) 后再传递进去 如果一个list传递给lisp宏,则不被求值,而将其完整的传递进去,至于 宏里面怎么干,随便宏的实现者怎么玩。像C的宏吧,不过C的宏只是文本替换,还是简 单了点。
- 宏可以返回的是一个list,而且被视作可以求值的list,也就是代码。
- 两阶段执行,第一阶段在编译期,称之为展开,第二阶段在运行期,称之为计算。宏在 展开时,并不对实参求值,只把宏定义中对形参的引用简单替换为实参。实参在计算阶 段时才求值。
Elixir是什么
Elixir 是一个基于Erlang虚拟机强大的类Ruby语法的编程语言。
Elixir的宏
Elixir也是支持宏的,并且Elixir的宏也是异常强大的,也做到了两阶段执行。 但是今天 要介绍的主要是关于Elixir中use和@before_compile的部分。
代码例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
defmodule MyModule do use MyPlugBuilder plug :hello plug :world, good: :morning end defmodule MyPlugBuilder do defmacro __using__(_opts) do quote do import MyPlugBuilder, only: [plug: 1, plug: 2] Module.register_attribute(__MODULE__, :plugs, accumulate: :true) @before_compile MyPlugBuilder end end defmacro plug(plug, opts \\ []) do quote do @plugs {unquote(plug), unquote(opts)} end end defmacro __before_compile__(env) do plugs = Module.get_attribute(env.module, :plugs) quote do def plugs, do: unquote(plugs) end end end |
MyPlugBuilder的展开
第一步
1 2 3 4 5 6 7 8 9 10 11 |
defmodule MyModule do # ---- # use MyPlugBuilder # ---- ↓ require MyPlugBuilder MyPlugBuilder.__using__([]) # ---- plug :hello plug :world, good: :morning end |
因为use MyPlugBuilder这句话会展开成 require MyPlugBuilder MyPlugBuilder.using MyModule 会请求引入 MyPlugBuilder,接着会调用_using_宏,并且默认参数为[]
第二步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
defmodule MyModule do require MyPlugBuilder # ---- # MyPlugBuilder.__using__([]) # ---- ↓ import MyPlugBuilder, only: [plug: 1, plug: 2] Module.register_attribute(__MODULE__, :plugs, accumulate: :true) @before_compile MyPlugBuilder # ---- plug :hello plug :world, good: :morning end |
using宏会立刻被执行,相当于立刻将MyPlugBuilder的函数引入进来,并且给MyModule 注册了一个叫做plugs的模块属性。同时告诉编译器,稍后编译MyPlugBuilder的时候,调 用_before_compile_。
第三步
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
defmodule MyModule do require MyPlugBuilder import MyPlugBuilder, only: [plug: 1, plug: 2] Module.register_attribute(__MODULE__, :plugs, accumulate: :true) @before_compile MyPlugBuilder # ---- # plug :hello # plug :world, good: :morning # ---- ↓ @plugs {:hello, []} @plugs {:world, [good: :morning]} # ---- end |
此时还没有展开_before_compile_,而是先展开从MyPlugBuilder模块中import进来的 plug宏,完成相关定义内容
第四步
1 2 3 4 5 6 7 8 9 10 11 |
defmodule MyModule do require MyPlugBuilder import MyPlugBuilder, only: [plug: 1, plug: 2] Module.register_attribute(__MODULE__, :plugs, accumulate: :true) @before_compile MyPlugBuilder @plugs {:hello, []} @plugs {:world, [good: :morning]} MyPlugBuilder.__before_compile__(__ENV__) end |
此时展开了MyPlugBuilder中_before_compile_宏,完成整个展开过程。
一个复杂点的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
defmodule MyPlugBuilder do defmacro __using__(_opts) do quote do import MyPlugBuilder, only: [plug: 1, plug: 2, aplug: 1] Module.register_attribute(__MODULE__, :plugs, accumulate: :true) IO.puts __MODULE__ IO.puts unquote(__MODULE__) @before_compile MyPlugBuilder unquote(defs()) end end # `plug` 本体 defmacro plug(plug, opts \\ []) do quote do IO.puts unquote(plug) @plugs {unquote(plug), unquote(opts)} end end defmacro aplug(plug) do xplug(plug, []) end defp defs() do IO.puts "aplug" quote unquote: false do IO.puts "eval" var!(pplug, MyPlugBuilder) = fn resource -> IO.puts resource end end end defp xplug(plug,opts \\ []) do quote do plug = unquote(plug) var!(pplug, MyPlugBuilder).(plug) end end defmacro __before_compile__(env) do plugs = Module.get_attribute(env.module, :plugs) IO.puts "__before_compile__" conn = compile(env) quote do def plugs, do: unquote(plugs) def plug_builder_call(unquote(conn)), do: IO.puts conn end end def compile(env) do conn = quote do: conn conn end end |
展开后代码上的差异
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
defmodule MyModule do # ---- # use MyPlugBuilder # ---- ↓ require MyPlugBuilder MyPlugBuilder.__using__([]) # ---- plug :hello plug :world, good: :morning end defmodule MyModule do require MyPlugBuilder # ---- # MyPlugBuilder.__using__([]) # ---- ↓ MyPlugBuilder.defs() import MyPlugBuilder, only: [plug: 1, plug: 2, aplug: 1] Module.register_attribute(__MODULE__, :plugs, accumulate: :true) IO.puts __MODULE__ IO.puts "MyPlugBuilder" @before_compile MyPlugBuilder # ---- plug :hello plug :world, good: :morning end |
至此读者可能已经注意到了MyPlugBuilder的defs()函数先于两个IO.puts执行了。
总结
可以在编译期间展开在执行阶段求值实参的宏,确实可以给我们带来很大的方便,但是也大 大带来了危险性。 宏乃屠龙之技,但是用的时候要慎之再慎。