我有一张地图,我想对几个功能使用单一的真实来源。让我们说它是:
source_of_truth = %{a: 10, b: 20}
Run Code Online (Sandbox Code Playgroud)
我希望该地图的键是EctoEnum 的值。EctoEnum 提供了一个defenum我应该像这样使用的宏:
defenum(
EnumModule,
:enum_name,
[:a, :b]
)
Run Code Online (Sandbox Code Playgroud)
我不想重复[:a, :b]部分。我想像这样使用地图中的键:
defenum(
EnumModule,
:enum_name,
Map.keys(source_of_truth)
)
Run Code Online (Sandbox Code Playgroud)
它不起作用,因为defenum宏需要一个简单的列表。
我想我可以通过像这样定义我自己的宏来做到这一点:
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(enum_values)
)
end
end
Run Code Online (Sandbox Code Playgroud)
然后调用:
dynamic_enum(EnumModule, :enum_name, Map.keys(source_of_truth))
Run Code Online (Sandbox Code Playgroud)
然而,它做同样的事情:enum_values不是预先计算的列表,而是 AST for Map.get. 我的下一个方法是:
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
quote do
values = unquote(enum_values)
defenum(
unquote(enum_module),
unquote(enum_name),
?
)
end
end
Run Code Online (Sandbox Code Playgroud)
不知道我能把什么放在哪里?。我不能只是values因为它是一个变量而不是一个列表。我不能把unquote(values)两种。
一种有效的解决方案是这样的:
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
{values, _} = Code.eval_quoted(enum_values)
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(values)
)
end
end
Run Code Online (Sandbox Code Playgroud)
但是,文档说eval_quoted在宏内部使用是一种不好的做法。
[编辑] 解决方案Macro.expand也不起作用,因为它实际上没有评估任何东西。扩展停止于:
Expanded: {{:., [],
[
{:__aliases__, [alias: false, counter: -576460752303357631], [:Module]},
:get_attribute
]}, [],
[
{:__MODULE__, [counter: -576460752303357631], Kernel},
:keys,
[
{:{}, [],
[
TestModule,
:__MODULE__,
0,
[
file: '...',
line: 16
]
]}
]
]}
Run Code Online (Sandbox Code Playgroud)
所以它没有像我们预期的那样扩展到列表中。
[\编辑]
什么是解决该问题的好方法?
如文档中所述 Macro.expand/2
扩充内容如下:
- 宏(本地或远程)
- 扩展别名(如果可能)并返回原子
- 编译环境宏(
__CALLER__/0、__DIR__/0、__ENV__/0和__MODULE__/0)- 模块属性读取器 (
@foo)
重点是我的。因此,可能将模块属性与Macro.expand/2.
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
IO.inspect(enum_values, label: "Passed")
expanded = Macro.expand(enum_values, __CALLER__)
IO.inspect(expanded, label: "Expanded")
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(expanded)
)
end
end
Run Code Online (Sandbox Code Playgroud)
并称之为:
@source_of_truth %{a: 10, b: 20}
@keys Map.keys(@source_of_truth)
def test_attr do
dynamic_enum(EnumModuleA, :enum_name_a, @keys)
end
Run Code Online (Sandbox Code Playgroud)
FWIW,完整代码:
$ \cat lib/eenum.ex
defmodule Eenum do
import EctoEnum
defmacro dynamic_enum(enum_module, enum_name, enum_values) do
IO.inspect(enum_values, label: "Passed")
expanded = Macro.expand(enum_values, __CALLER__)
IO.inspect(expanded, label: "Expanded")
quote do
defenum(
unquote(enum_module),
unquote(enum_name),
unquote(expanded)
)
end
end
end
Run Code Online (Sandbox Code Playgroud)
$ \cat lib/tester.ex
defmodule Tester do
import Eenum
@source_of_truth %{a: 10, b: 20}
@keys Map.keys(@source_of_truth)
def test_attr do
dynamic_enum(EnumModuleA, :enum_name_a, @keys)
end
end
Run Code Online (Sandbox Code Playgroud)
FWIW 2.为了能够从模块范围调用dynamic_enum如上所示,您需要的是(惊喜:)另一个模块范围,在宏调用时已经编译:
defmodule Defs do
@source_of_truth %{a: 10, b: 20}
@keys Map.keys(@source_of_truth)
defmacro keys, do: Macro.expand(@keys, __CALLER__)
end
defmodule Tester do
import Defs
import Eenum
dynamic_enum(EnumModuleA, :enum_name_a, keys())
end
Run Code Online (Sandbox Code Playgroud)
FWIW 3. 后者(带有定义的显式模块)即使不需要模块属性也能工作:
defmodule Defs do
defmacro keys, do: Macro.expand(Map.keys(%{a: 10, b: 20}), __CALLER__)
end
defmodule Tester do
import Defs
import Eenum
dynamic_enum(EnumModuleA, :enum_name_a, keys())
end
Run Code Online (Sandbox Code Playgroud)
经验法则是,当您发现自己需要调用时Code.eval_quoted/3,将这段代码放入独立模块中,让编译器为您调用这段代码编译。对于函数在模块级别上工作,对于模块级别,它应该放入另一个模块以使模块上下文(又名__CALLER__和__ENV__)可用。