[Elixir Macro #2]编译时和运行时
首先,先确定下本文中的这两个概念:
编译时:从 mix compile
命令开始到该命令结束,是解释源码,编译过程生成 beam 文件的过程
运行时:VM 运行 beam 中间代码的过程,和源码文件无关
在自己写代码或读别人代码的时候,弄明白一个变量或一个函数是编译时起作用的还是运行时起作用的,非常重要。
编译时变量和运行时变量
再次引入几个概念:
- L2 的
@a
定义为「模块属性」 - L3 的
x
L15 的y
定义为「模块变量」 - 「模块变量」和「模块属性」都定义为「编译时变量」
下面例子中的 @a
x
y
都是编译时的,特别注意下 L14 和 L17,「模块变量」和「模块属性」,在作用域方面的区别:
defmodule M do @a 1 x = 2 def f1 do IO.inspect @a end def f2 do IO.inspect unquote(x) end defmacro m1 do x = unquote(x) y = 3 quote do IO.inspect {unquote(@a), unquote(x), unquote(y)} end end end defmodule N do require M M.f1 M.f2 M.m1 end
以上例子中,没有运行时变量,因为其中的函数都是无参函数,而「运行时变量」是指「函数的参数」。
简单的总结:
- 编译时变量与 module 绑定,生命周期从 defmodule 开始,到该 module 编译完成,生成 beam 文件结束。
- 编译时变量有两种写法,「模块变量」和「模块属性」。模块属性是整个 module 通用的,模块变量的作用域仅在当前 block。
- 编译时变量最终通过 unquote 内联编译到 module 中。
- 运行时变量是函数的参数,与业务逻辑和用户行为有关,编译过程拿不到变量值,更不会修改变量值。
编译时函数和运行时函数
这次的例子是从 elixir 的源码中抄了一段,实现的是 elixir 里的管道 |>
,源码在 这里 :
@doc """ Breaks a pipeline expression into a list. Raises if the pipeline is ill-formed. """ @spec unpipe(Macro.t) :: [Macro.t] def unpipe(expr) do :lists.reverse(unpipe(expr, [])) end defp unpipe({:|>, _, [left, right]}, acc) do unpipe(right, unpipe(left, acc)) end defp unpipe(other, acc) do [{other, 0}|acc] end @doc """ Pipes `expr` into the `call_args` at the given `position`. """ @spec pipe(Macro.t, Macro.t, integer) :: Macro.t | no_return def pipe(expr, call_args, position) def pipe(expr, {:&, _, _} = call_args, _integer) do raise ArgumentError, bad_pipe(expr, call_args) end def pipe(expr, {tuple_or_map, _, _} = call_args, _integer) when tuple_or_map in [:{}, :%{}] do raise ArgumentError, bad_pipe(expr, call_args) end def pipe(expr, {call, _, [_, _]} = call_args, _integer) when call in unquote(@binary_ops) do raise ArgumentError, "cannot pipe #{to_string expr} into #{to_string call_args}, " <> "the #{to_string call} operator can only take two arguments" end def pipe(expr, {call, line, atom}, integer) when is_atom(atom) do {call, line, List.insert_at([], integer, expr)} end def pipe(expr, {call, line, args}, integer) when is_list(args) do {call, line, List.insert_at(args, integer, expr)} end def pipe(expr, call_args, _integer) do raise ArgumentError, bad_pipe(expr, call_args) end defp bad_pipe(expr, call_args) do "cannot pipe #{to_string expr} into #{to_string call_args}, " <> "can only pipe into local calls foo(), remote calls Foo.bar() or anonymous functions calls foo.()" end
这里的重点还是放在了「编译时函数」,因为「运行时函数」就是普通的函数,处理业务逻辑。
在上面例子中,pipe 和 unpipe 函数的输入输出都是 AST,只不过输出的 AST 没有用 quote 来生成,是用的上一篇中所说的「手写」+「组合」的方式。
简单的总结:
- 编译时函数是处理 AST 的函数,输入输出都是 AST。
- 编译时函数在编译阶段执行,编译完成后函数依然存在,但不会在运行时被调用。
- 编译时函数通常在宏中被调用。之前说过宏的输入输出也都是 AST,如果宏处理 AST 的逻辑过于复杂,会引入单独的函数来处理。
- 运行时函数是处理业务逻辑的函数,不在编译过程中被调用。
编译时编程和运行时编程
编译时编程和运行时编程最大的区别:编译时编程不是函数式的,存在全局变量!
DSL 的编写逻辑,是定义全局变量、更新全局变量、处理全局变量的过程:
- 定义模块属性变量(上面提到的全局变量)。
- 编写相应的宏,修改模块属性。
- 在
__before_compile__
中取出模块属性,格式化成对应的函数或语句。
这一条可能有点抽象,也没有举代码的例子,因为接下来两篇文章,是两个很有代表性的「宏」的使用场景和使用方式的例子,是对这一条的详细解释。
PS. 文中有些用词非常不妥,那些「定义」也并非通用定义,仅限在本系列文章中有效。比如「模块属性」,这里 有官方的详细教程,还有其它的定义和用途,本文有些以偏概全;再比如「全局变量」,这只是为了做一个容易理解的近似描述。请不要挑剔这些细节,领会精神即可,当然如果有更好的表达,欢迎拍砖。