[Erlang]带你了解Elixir的宏

发布于:2019-10-12

Erlang

什么宏

宏(Macro),是一种批量处理的称谓。一般说来,宏是一种规则或模式,或称语法替换 ,用于说明某一特定输入(通常是字符串)如何根据预定义的规则转换成对应的输出(通常也是字符串)。这种替换在预编译时进行,称作宏展开。 说道宏,就不得不提一个经典语言和它的宏。

Lisp

Lisp的特点

  1. 数据就代码,代码就是数据
  2. LISP中所有的都是list, 当然也可以叫做S表达式。
  3. 如果把list中的第一元素视为函数,该list就可视作代码一样运行。术语叫做求值, evaluate。当然也可以不求值,此时list就是数据。因此这里引出一个重要概念 ,代码也是数据,一切皆为数据,一切都是list。

Lisp的宏

  1. 如果一个list传递给lisp函数,则先被求值为atom(一个特殊的list,不能再被求值)后再传递进去 如果一个list传递给lisp宏,则不被求值,而将其完整的传递进去,至于宏里面怎么干,随便宏的实现者怎么玩。像C的宏吧,不过C的宏只是文本替换,还是简单了点。
  2. 宏可以返回的是一个list,而且被视作可以求值的list,也就是代码。
  3. 两阶段执行,第一阶段在编译期,称之为展开,第二阶段在运行期,称之为计算。宏在展开时,并不对实参求值,只把宏定义中对形参的引用简单替换为实参。实参在计算阶段时才求值。

Elixir是什么

Elixir 是一个基于Erlang虚拟机强大的类Ruby语法的编程语言。

Elixir的宏

Elixir也是支持宏的,并且Elixir的宏也是异常强大的,也做到了两阶段执行。 但是今天要介绍的主要是关于Elixir中use和@before_compile的部分。

代码例子

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的展开

第一步


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__宏,并且默认参数为[]

第二步


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__

第三步

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宏,完成相关定义内容

第四步

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__宏,完成整个展开过程。

一个复杂点的例子


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

展开后代码上的差异


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执行了。

总结

可以在编译期间展开在执行阶段求值实参的宏,确实可以给我们带来很大的方便,但是也大大带来了危险性。 宏乃屠龙之技,但是用的时候要慎之再慎。

Elixir 1.1.1 代码分析(未完成)