[英]Passing computed list to an Elixir macro
我有一张地图,我想用几个功能的单一事实来源。 让我们说它是:
source_of_truth = %{a: 10, b: 20}
我希望该地图的键是EctoEnum的值。 EctoEnum提供了一个我应该使用的宏defenum
:
defenum(
EnumModule,
:enum_name,
[:a, :b]
)
我不想重复[:a, :b]
部分。 我想使用地图中的键而不是这样:
defenum(
EnumModule,
:enum_name,
Map.keys(source_of_truth)
)
它不起作用,因为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
然后打电话:
dynamic_enum(EnumModule, :enum_name, Map.keys(source_of_truth))
但是,它做同样的事情: enum_values
不是预先计算的列表,而是Map.get
AST。 我的下一个方法是:
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
不知道我能把它放在哪里?
是。 我不能只放置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
但是,文档说在宏中使用eval_quoted
是一种不好的做法。
[编辑]使用Macro.expand
的解决方案也不起作用,因为它实际上没有评估任何内容。 扩张停在:
Expanded: {{:., [],
[
{:__aliases__, [alias: false, counter: -576460752303357631], [:Module]},
:get_attribute
]}, [],
[
{:__MODULE__, [counter: -576460752303357631], Kernel},
:keys,
[
{:{}, [],
[
TestModule,
:__MODULE__,
0,
[
file: '...',
line: 16
]
]}
]
]}
所以它没有像我们预期的那样扩展到列表。
[\\编辑]
这个问题有什么好的解决方案?
如Macro.expand/2
的文档中所述
扩展了以下内容:
- 宏(本地或远程)
- 别名扩展(如果可能)并返回原子
- 编译环境宏(
__CALLER__/0
,__DIR__/0
__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
称之为:
@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
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
$ \\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
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
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
经验法则是当您发现自己需要调用Code.eval_quoted/3
,将此代码放入独立模块并让编译器为您调用此代码编译。 对于函数是在模块级别工作,对于模块级别,它应该放入另一个模块以使模块上下文(aka __CALLER__
和__ENV__
)可用。
我曾经和同样的问题争吵了一会儿。 基本上,您可以在quote
构建语法树,使用unquote
注入动态值,然后使用Code.eval_quoted
来评估宏:
options = Map.keys(source_of_truth)
Code.eval_quoted(
quote do
EctoEnum.defenum(MyEnum, :type_name, unquote(options))
end,
[],
__ENV__
)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.