#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
Надеюсь, некоторые идеи окажутся полезными для вашей ситуации.