Передача вычисленного списка в макрос Elixir - PullRequest
0 голосов
/ 06 декабря 2018

У меня есть карта, на которой я хочу использовать один источник правды для нескольких функций.Допустим, это:

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 - это не предварительно вычисленный список, а AST для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

Не уверен, что я мог бы поместить туда, где находится ?.Я не могу просто поставить 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 внутри макросаплохая практика.

[EDIT] Решение с Macro.expand тоже не работает, потому что на самом деле ничего не оценивает.Расширение останавливается на:

Expanded: {{:., [],
  [
    {:__aliases__, [alias: false, counter: -576460752303357631], [:Module]},
    :get_attribute
  ]}, [],
 [
   {:__MODULE__, [counter: -576460752303357631], Kernel},
   :keys,
   [
     {:{}, [],
      [
        TestModule,
        :__MODULE__,
        0,
        [
          file: '...',
          line: 16
        ]
      ]}
   ]
 ]}

, поэтому оно не расширяется до списка, как мы ожидали.

[\ EDIT]

Что является хорошим решением для этой проблемы

Ответы [ 2 ]

0 голосов
/ 07 декабря 2018

Как указано в документации для 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

и назвать его следующим образом:

  @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, поместите этот код вНезависимый модуль и компилятор make вызывают этот код для вас.Для функций это работа на уровне модуля, для уровня модуля это должно быть помещено в другой модуль, чтобы сделать контекст модуля (также известный как __CALLER__ и __ENV__) доступным.

0 голосов
/ 06 декабря 2018

Я боролся с той же проблемой некоторое время назад.По сути, вы можете построить свое синтаксическое дерево в 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__
)
...