用于模块属性的 Elixir 元编程

lap*_*ira 1 macros metaprogramming elixir

我有这个模块

defmodule ElixirMeta.LangLoader do

  @external_resource [Path.join([__DIR__, "es.json"]),
                      Path.join([__DIR__, "en.json"])]

  defmacro __using__(_) do
    for lang <- ["es", "en"] do
      {:ok, body} = File.read(Path.join([__DIR__, "#{lang}.json"]))
      {:ok, json} = Poison.decode(body)
      quote do
        def lang(unquote(lang)), do: unquote(Macro.escape(json))
      end
    end
  end
end

defmodule ElixirMeta.Lang do
  use ElixirMeta.LangLoader
end
Run Code Online (Sandbox Code Playgroud)

我知道我可以定义一个函数,如:

def lang(unquote(lang)), do: unquote(Macro.escape(json))
Run Code Online (Sandbox Code Playgroud)

并且可以这样调用:

Lang.lang("es")
Run Code Online (Sandbox Code Playgroud)

甚至修改它的函数名,像这样:

def unquote(:"lang_#{lang}")(), do: unquote(Macro.escape(json))
Run Code Online (Sandbox Code Playgroud)

并像这样被调用:

Lang.lang_es
Run Code Online (Sandbox Code Playgroud)

但是可以对模块属性做同样的事情吗?

并且作为模块属性编译(?)我认为不可能从宏初始化它?也许我必须在before_compile宏中做到这一点?

出于示例的目的,我想访问Lang.lang_es作为 a@lang_es@lang_en LangLoader属性

Ale*_*kin 5

是的,可以通过以下方式实现Module.put_attribute/3(我已经根据您的初始代码创建了一个 MCVE):

defmodule ElixirMeta.LangLoader do
  defmacro __using__(_) do
    [
      (quote do: Module.register_attribute __MODULE__,
        :langs, accumulate: true) |
      for lang <- ["es", "en"] do
        quote do
          def lang(unquote(lang)), do: unquote(lang)
          Module.put_attribute __MODULE__,
            :"lang_#{unquote(lang)}", unquote(lang)
          Module.put_attribute __MODULE__,
            :langs, unquote(lang)
        end
      end
    ]
  end
end

defmodule ElixirMeta.Lang do
  use ElixirMeta.LangLoader

  def test do
    IO.inspect {
      @lang_es,
      Enum.find(@langs, & &1 == "es"),
      lang("es")
    }, label: "Variants"
  end
end

ElixirMeta.Lang.test
#? Variants: {"es", "es", "es"}
Run Code Online (Sandbox Code Playgroud)

上面的代码声明了累积属性(@attr :foo后跟@attr :bar会产生[:foo, :bar]值而不是覆盖属性值、单个属性和函数。

请注意,无法从外部访问模块属性,因为模块属性是编译时实体。