【问题标题】:Using helper functions in Elixir macros在 Elixir 宏中使用辅助函数
【发布时间】:2016-04-25 20:13:24
【问题描述】:

继续 Elixir 文档中的 Binding and unquote fragments 示例...

我们有一个基于关键字列表定义函数的宏。

defmodule MacroFun do

  defmacro defkv(kv) do
    quote bind_quoted: [kv: kv] do
      Enum.each kv, fn {k, v} ->
        def unquote(k)(), do: unquote(v)
      end
    end
  end

end

defmodule Runner do
  require MacroFun

  kv = [foo: 1, bar: 2]
  MacroFun.defkv(kv)

end

Runner.foo

现在让我们将宏的主体移动到辅助函数中。

  defmacro defkv(kv) do
    _defkv(kv)
  end

  defp _defkv(kv) do
    quote bind_quoted: [kv: kv] do
      Enum.each kv, fn {k, v} ->
        def unquote(k)(), do: unquote(v)
      end
    end
  end

很好,一切仍然有效。但是现在如果我们想在将kv 传递给私有辅助函数之前创建另一个修改kv 的宏怎么办:

  defmacro def_modified_kv(kv) do
    quote bind_quoted: [kv: kv] do
      modified_kv = Enum.map kv, fn {k, v} -> {k, v + 1} end
      _defkv(modified_kv)
    end
  end

这行不通。 Elixir 说 _devkv 没有定义。我们可以通过使用完全限定的函数名来修复:

  defmacro def_modified_kv(kv) do
    quote bind_quoted: [kv: kv] do
      modified_kv = Enum.map kv, fn {k, v} -> {k, v + 1} end
      MacroFun._defkv(modified_kv)
    end
  end

但后来 Elixir 抱怨 MacroFun._defkv 是私有的。所以我们把它改成 public,但是它仍然不起作用,因为辅助方法 _devkv 将引用的代码返回给我们的宏 def_modified_kv,它本身被引用了!

所以我们可以通过评估辅助函数返回的代码(最终代码)来解决这个问题:

defmodule MacroFun do

  defmacro defkv(kv) do
    _defkv(kv)
  end

  defmacro def_modified_kv(kv) do
    quote bind_quoted: [kv: kv] do
      modified_kv = Enum.map kv, fn {k, v} -> {k, v + 1} end
      MacroFun._defkv(modified_kv) |> Code.eval_quoted([], __ENV__) |> elem(0)
    end
  end

  def _defkv(kv) do
    quote bind_quoted: [kv: kv] do
      Enum.each kv, fn {k, v} ->
        def unquote(k)(), do: unquote(v)
      end
    end
  end

end
  1. 为什么我必须改为使用完全限定名称调用辅助函数?
  2. 为什么我必须将辅助函数更改为公共(从私有)?
  3. 除了调用Code.eval_quoted之外还有更好的方法吗?

我觉得我做错了什么。

感谢您的帮助。

【问题讨论】:

    标签: macros metaprogramming elixir


    【解决方案1】:
    1. 您需要导入模块。
    2. 导入模块时,只能导入公共方法。

    3. 宏是一种编译时特性,它们在模块中编写实际编写代码(或 AST)。因此,宏展开时的上下文就是您调用/使用它的地方。因此,如果您使用宏(未在其中定义)SomeOtherModule,那就是宏扩展的上下文。在SomeOtherModule 中,您需要import MacroFun 才能在本地调用其函数。

    以下代码适用于 Elixir >= 1.2.0

    例如

    defmodule MacroFun do
    
      defmacro defkv(kv) do
        _defkv(kv)
      end
    
      defp _defkv(kv) do
        quote bind_quoted: [kv: kv] do
          Enum.each kv, fn {k, v} ->
            def unquote(k)(), do: unquote(v)
          end
        end
      end
    
      defmacro def_modified_kv(kv) do
        quote bind_quoted: [kv: kv] do
          modified_kv = Enum.map kv, fn {k, v} -> {k, v + 1} end
          defkv(modified_kv)
        end
      end
    end
    
    defmodule Runner do
      import MacroFun
    
      kv = [foo: 1, bar: 2]
      def_modified_kv(kv)
    
    end
    

    希望这能回答您的问题!祝你好运。

    更新了几次,因为宏有点混乱!

    【讨论】:

    • 所以基本上,你做的两件事不同的是 1) 你叫 public defkv macro private _defkv function, 2) 你使用 import 而不是 require跨度>
    • 不是require,而不是import,当我们要使用另一个模块定义的宏时应该调用它?为什么import 在这种情况下有效?
    • require 只是一个带有宏的模块——它实际上只是导入模块。 import 完成同样的事情,检查 AST。
    【解决方案2】:

    1) 通过使用require,您告诉编译器编译并提供MacroFuns public 宏和函数。请注意,这些功能无论如何都是可用的:

    请注意,在使用之前通常不需要模块,唯一的 如果您想使用模块中的宏,则例外。

    2) 它们可以通过 FQN 获得

    3) 这是我的看法:

    defmodule MicroFun do
      defmacro defkv(kv) do
        _defkv(kv)
      end
    
      defp _defkv(kv) do
        quote bind_quoted: [kv: kv] do
          Enum.each kv, fn {k,v} ->
            def unquote(k)(), do: unquote(v)
          end
        end
      end
    
      defmacro modifier(kv) do
        quote bind_quoted: [kv: kv] do
          Enum.map kv, fn {k,v} -> {k, v+1} end
        end
      end
    end
    
    defmodule Runner do
      require MicroFun
    
      [foo: 1, bar: 2]
      |> MicroFun.modifier
      |> MicroFun.defkv
    end
    

    这个想法是,当您可以通过管道传递宏/函数时,您不需要嵌套它们。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-01-17
      • 1970-01-01
      • 2018-05-07
      • 2019-11-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多