将模块属性作为参数传递给宏

时间:2019-01-02 00:42:53

标签: elixir

它是Elixir中的common to have constants as Module Attributes。我试图将模块属性作为参数传递给来自不同库(通常定义一个新模块)的不同宏:

defmodule X do
  @data_types [:x, :y, :z]
  @another_constant "some-constant-value"

  defenum DataType, :type, @data_types
end

这是passing module attributes as an argument to a Macro

的另一个示例

但几乎总是会出现相同的错误:

** (Protocol.UndefinedError) protocol Enumerable not implemented for {:@, [line: 26], [{:types, [line: 26], nil}]}

所以我通常最终会重复这些值:

defmodule X do
  @data_types [:x, :y, :z]
  @another_constant "some-constant-value"

  defenum DataType, :type, [:x, :y, :z]
end

我知道大多数时候重复一次通常没什么大不了的,但是我真的很想知道如何将模块属性的值传递给宏。

这在定义新模块(例如AmnesiaEctoEnum)的宏中尤为明显。


到目前为止,我已经尝试了很多方法,包括:

  • 使用Macro模块扩展值
  • 使用Code模块评估值
  • 使用Module.get_attribute/2
  • 获取值
  • 尝试不同的报价/取消报价呼叫

但是没有任何效果。我感觉需要以一种可以读取宏的方式编写宏。如果是这样,应该如何编写宏才能使其工作?

2 个答案:

答案 0 :(得分:1)

不幸的是,通过将任意带引号的表达式传递到外部库来解决此问题的唯一方法是提供在库中解决问题的请求请求

考虑以下示例

defmodule Macros do
  defmacro good(param) do
    IO.inspect(param, label: " Passed")
    expanded = Macro.expand(param, __CALLER__)
    IO.inspect(expanded, label: " Expanded")
  end

  defmacro bad(param) do
    IO.inspect(param, label: " Not Expanded")
  end
end

defmodule Test do
  import Macros
  @data_types [:x, :y, :z]

  def test do
    good(@data_types)
    bad(@data_types)
  end
end

Test的声明打印:

 Passed: {:@, [line: 28], [{:data_types, [line: 28], nil}]}
 Expanded: [:x, :y, :z]
 Not Expanded: {:@, [line: 29], [{:data_types, [line: 29], nil}]}

如果第三方库未在参数上调用Macro.expand/2,则引用的表达式将不会展开。以下是文档摘录:

  

以下内容已展开:

     

•宏(本地或远程)
  •别名被扩展(如果可能)并返回原子
  •编译环境宏(__CALLER__/0__DIR__/0__ENV__/0__MODULE__/0
  •模块属性读取器(@foo

也就是说,要具有接受模块属性或信号之类的宏调用的能力,第三方库宏必须对参数调用Macro.expand。您无法通过客户端代码解决此问题。

答案 1 :(得分:0)

  

但是几乎总是会出错...

在此示例中,我可以访问宏内的模块属性:

defmodule My do
  @data_types [:x, :y, :z]

  defmacro go() do
    quote do
      def types() do
        unquote(@data_types)
      end
    end
  end
end

defmodule Test do
  require My
  My.go()

  def start do
    types()
  end
end

在iex中:

~/elixir_programs$ iex my.exs
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> Test.start()
[:x, :y, :z]

iex(2)> 

回复评论:

下面是一个将模块属性作为参数传递给宏函数调用的示例:

defmodule My do

  defmacro go(arg) do
    quote do
      def show do
        Enum.each(unquote(arg), fn x -> IO.inspect x end)
      end
    end
  end

end

defmodule Test do
  @data_types [:x, :y, :z]

  require My
  My.go(@data_types)

  def start do
    show()
  end

end

在iex中:

~/elixir_programs$ iex my.exs
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]
Interactive Elixir (1.6.6) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)> Test.start()
:x
:y
:z
:ok

iex(2)> 

如果相反,请尝试以下操作:

defmodule My do
  defmacro go(arg) do
    Enum.each(arg, fn x -> IO.inspect x end)
  end

end

defmodule Test do
  require My
  @data_types [:x, :y, :z]
  My.go(@data_types)

  def start do
    show()
  end
end

然后在iex中会出现错误:

~/elixir_programs$ iex my.exs
Erlang/OTP 20 [erts-9.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

** (Protocol.UndefinedError) protocol Enumerable not implemented for {:@, [line: 11], [{:data_types, [line: 11], nil}]}
    (elixir) lib/enum.ex:1: Enumerable.impl_for!/1
    (elixir) lib/enum.ex:141: Enumerable.reduce/3
    (elixir) lib/enum.ex:1919: Enum.each/2
    expanding macro: My.go/1
    my.exs:11: Test (module)

发生该错误是因为您试图枚举ast:

{:@, [line: 11], [{:data_types, [line: 11], nil}]}

这不是一个列表(或任何其他可枚举的-显然是一个元组!)。请记住,宏的参数是ast的。

此方法起作用的原因:

  defmacro go(arg) do

    quote do
      def show do
        Enum.each(unquote(arg), fn x -> IO.inspect x end)
      end
    end

  end

是因为quote()创建了一个ast,而unquote(arg)将其他ast注入了ast的中间。宏调用My.go(@data_types)在编译时执行,并且elixir用My.go()返回的ast替换宏调用,show()是名为{{1}}的函数定义。