将Module属性作为参数传递给宏



在Elixir中,将常量作为模块属性是很常见的。我试图将模块属性作为参数传递给来自不同库的不同宏(通常定义一个新模块):

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

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


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

** (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获取值
  • 尝试引用/取消引用调用的不同变体

但什么都没用。我有一种感觉,宏需要以一种可以阅读它们的方式编写。如果是,应该如何编写宏以使其工作?

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

考虑以下示例

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)

也就是说,要能够接受模块属性或宏调用(如sigils),第三方库宏必须对参数调用Macro.expand。您无法从客户端代码中修复此问题。

但几乎总是会出错。。。

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

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()的函数定义。

最新更新