【问题标题】:Passing computed list to an Elixir macro将计算列表传递给 Elixir 宏
【发布时间】:2018-12-08 11:57:37
【问题描述】:

我有一张地图,我想将单一事实来源用于几个功能。假设它是:

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
        ]
      ]}
   ]
 ]}

所以它没有像我们预期的那样扩展到列表。

[\编辑]

什么是解决这个问题的好方法?

【问题讨论】:

    标签: macros elixir


    【解决方案1】:

    Macro.expand/2 的文档中所述

    展开如下内容:

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

    重点是我的。因此,可以将 module attributesMacro.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时,把这段代码放到独立模块中,让编译器为你调用这段代码编译。对于功能是在模块级别上工作的,对于模块级别,它应该被放入另一个模块中以使模块上下文(又名__CALLER____ENV__)可用。

    【讨论】:

    • 那行不通。它扩展 AST 直到不再可能扩展,但它不会评估列表。我编辑了问题以显示它会导致什么错误。
    • 我很确定它对我有用;我已经测试过了。你确定你通过了@keys,它是__CALLER__ 上下文中定义的列表
    • FWIW,我已经发布了我用于测试的完整代码。
    • 永远欢迎。跟进:事情是__CALLER__ context 是一个实际存储的 context / environment,并且需要实际的模块来存储它,因为不能附加任何任意数据,说,功能。这就是模块属性起作用而函数调用不起作用的原因。
    • 那就更好了!我的下一个问题是如何将定义移动到另一个模块,因为source_of_truth 可能很大!太棒了!
    【解决方案2】:

    不久前我曾与同样的问题作斗争。基本上你可以在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__
    )
    

    【讨论】:

    • 所以你建议不要在模块正文中使用defmacro,而只使用eval_quoted?我还有其他几个非常相似的情况,所以我想在宏中准备好代码。
    • 是的,我不认为它那么冗长。请注意,这可以转换为单行,我只是在此处将其列出以使其更清晰。但是我承认我没有想出一个方法来把它整齐地包装成一个宏。
    • 经验法则是当你发现自己需要调用Code.eval_quoted/3时,把这段代码放到独立的模块中,让编译器为你调用这段代码编译。对于功能是在模块级别上工作的,对于模块级别,它应该被放入另一个模块中以使模块上下文(又名__CALLER____ENV__)可用。
    猜你喜欢
    • 2011-07-12
    • 2016-09-03
    • 1970-01-01
    • 2015-08-03
    • 2020-07-17
    • 2019-11-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多