Эликсир вар! … Как прочитать переменную из области вызывающего объекта

#elixir #metaprogramming #hygiene

Вопрос:

Извините, если об этом уже спрашивали. Поиск по форуму var! дает мне все сообщения со словом var . Из-за этого было трудно сузить круг поисков.

Борется с написанием макроса, который считывает переменную из контекста вызывающего и возвращает ее из функции. Вот самая простая форма проблемы, которую я мог придумать:

 defmodule MyUnhygienicMacros do

  defmacro create_get_function do
    quote do
      def get_my_var do
        var!(my_var)
      end
    end
  end

end

defmodule Caller do

  require MyUnhygienicMacros

  my_var = "happy"

  MyUnhygienicMacros.create_get_function()

end
 

Цель состояла бы в том, чтобы увидеть это, когда я запускаю сеанс iex:

 $ Caller.get_my_var()
"happy"
 

Но это не компилируется. Звонящий тоже my_var не используется.

Ошибка компиляции expected "my_var" to expand to an existing variable or be part of a match.

Я прочитал книгу Маккорда по метапрограммированию, это сообщение в блоге (https://www.theerlangelist.com/article/macros_6) и многие другие. Кажется, это должно сработать, но я просто не могу понять, почему этого не произойдет..

Ответ №1:

Kernel.var!/2 макрос делает не то, что вы думаете.

Единственная цель var!/2 состоит в том, чтобы отметить переменную в макро-гигиене. Это означает, что использование var!/2 одного может изменить значение переменной во внешней (в отношении текущего контекста) области. В вашем примере есть две области ( defmacro[create_get_function] и def[get_my_var] ) для обхода, поэтому my_var не проходит.

Вся проблема выглядит как XY-проблема. Похоже, вы хотите объявить переменную времени компиляции и изменить ее на протяжении всего кода модуля. Для этой цели у нас есть атрибуты модуля accumulate: true .

Если вы хотите просто использовать эту переменную в create_get_function/0 , просто unquote/1 используйте ее. Если вы хотите накопить значение, используйте атрибуты модуля. Если вы все еще в конечном счете хотите сохранить все по-своему, передавая локальную переменную времени компиляции, дважды нарушите гигиену для обеих областей.

 defmodule MyUnhygienicMacros do
  defmacro create_get_function do
    quote do
      my_var_inner = var!(my_var)
      def get_my_var, do: var!(my_var_inner) = 42
      var!(my_var) = my_var_inner
    end
  end
end

defmodule Caller do
  require MyUnhygienicMacros
  my_var = "happy"
  MyUnhygienicMacros.create_get_function()
  IO.inspect(my_var, label: "modified?")
end
 

Пожалуйста, обратите внимание, что в отличие от того, что вы могли ожидать, приведенный выше код все еще печатается modified?: "happy" во время компиляции. Это происходит потому var!(my_var_inner) = 42 , что вызов будет удерживаться до времени выполнения, и обход гигиены макросов здесь будет невозможен.

Комментарии:

1. Вы прекрасно рассуждаете о том, что это проблема XY. Было две вещи, которые я пытался извлечь из этого поста: 1) Как решить мою реальную проблему (что я в конечном итоге сделал, используя атрибуты модуля и 2) лучшее понимание var!/1. Я не понимал, что есть два уровня контекста, через которые мне нужно пройти.

Ответ №2:

Взгляните на эти документы: https://hexdocs.pm/elixir/1.12/Kernel.Специальные формы.html#цитата/2

Есть множество примеров. Значение my_var определяется только внутри quote блока, но не внутри def функции внутри quote блока.

Вы могли бы сделать что-то вроде этого:

 defmodule MyUnhygienicMacros do
  defmacro create_get_function() do
    quote do
      @my_var var!(my_var)
      def get_my_var do
        @my_var
      end
    end
  end
end

defmodule Caller do
  require MyUnhygienicMacros
  my_var = "happy"
  MyUnhygienicMacros.create_get_function()
end

Caller.get_my_var()
|> IO.inspect()
 

и позвоните var! только внутри quote блока, назначив атрибут модуля @my_var .

Но я не очень хорош в метапрограммировании, и, вероятно, есть кто-то еще, кто мог бы ответить на него лучше.

Комментарии:

1. Это отличное начало — спасибо! Ваш код определенно работает, но теперь мне интересно, есть ли какой-либо способ сделать это, не загрязняя среду моего вызывающего абонента этим пользовательским атрибутом.

2. Вы здесь злоупотребляете var!/1 . Простого unquote/1 было бы вполне достаточно для этого примера.

3. Я согласен, что мой ответ странный и уродливый. Я просто хотел быть как можно ближе к операционному коду и заставить его работать. Существует слишком много способов подхода, чтобы я знал лучший подход, и мои навыки метапрограммирования недостаточно развиты. Для потомков прочтите ответ @AlekseiMatiushkin, он намного лучше моего.

Ответ №3:

Простите меня, если вы уже понимаете, что такое идиоматика в Эликсире и что может идти не по плану. Я представляю этот ответ в надежде, что он соответствует духу вашего вопроса.

Во-первых, все в Elixir является назначением, поэтому в большинстве случаев невозможно прочитать переменные за пределами области, в которой они были созданы. Вероятно, самой трудной вещью, от которой я отучился в те дни, когда занимался ОО-программированием, был простой шаблон (который очень похож на код в вашем вопросе).:

 # pseudo-code
x = "something"
foreach y in x {
  x = "something new"
}
 

Такой тип структуры не работает в Elixir-вам часто приходится использовать некоторые функции сопоставления или сокращения для достижения эквивалентного результата. Возможно, вам удастся обойти это ограничение с помощью макроса, но для этого, вероятно, должно быть действительно веское обоснование. Поэтому, возможно, вам следует переосмыслить, зачем вам нужна такая структура, или вы могли бы, как минимум, поделиться обоснованием, чтобы оно было понятно другим, читающим ваш вопрос.

Во-вторых, подумайте о передаче аргументов вашим макросам, когда необходимы значения-это помогает сделать область более очевидной. Вы можете использовать unquote для доступа к ним, например

 defmodule Foo do
  defmacro __using__(opts) do
    quote do 
      def get_thing(), do: unquote(opts[:thing])
    end
  end
end
 

Так что

 defmodule Bar do
  use Foo, thing: "blort"
end

defmodule Glop do
  use Foo, thing: "eesh"
end
 

позволит вам делать такие вещи, как это:

 Bar.get_thing() |> IO.puts() # "blort"
Glop.get_thing() |> IO.puts() # "eesh"
 

Я бы пришел к выводу, что выполнение чего-либо слишком причудливого или умного в ваших макросах может затруднить их отладку и обслуживание. Как бы то ни было, я обычно считал, что лучше всего сохранять макросы «тонкими» (например, тонкие контроллеры в приложениях MVC): по моему опыту, они лучше всего работают, когда они передаются другой обычной функции где-то в другом месте, например

   defmacro __using__(opts) do
    quote do
      def thing(x), do: Verbose.thing(unquote(opts[:arg1]), unquote(opts[:arg2]), x)

    end
  end   
 

Надеюсь, некоторые идеи окажутся полезными для вашей ситуации.