So I want to write some magic around Phoenix Channels and ran into Macro Hygiene problems along the way.
I want to make a macro handling message validation. It takes a do-block where the validated message is then processed. This do-block does not usually need access to the channel socket. My first attempt was to not var! the socket into its scope. I thought of having a separate macro to access the socket when needed.
Consider the following minimal example of my problem:
defmodule Mech do
defmacro foo(do: block) do
quote do
def fun(hygiene_var) do
unquote(block)
end
end
end
defmacro get_var do
quote do
hygiene_var
end
end
end
defmodule Test do
require Mech
import Mech
foo do
get_var() <> " bar"
end
end
Test.fun("foo") # expected output: "foo bar"
# Actual output: CompileError: variable "hygiene_var" does not exist
Mech.foo/1 is the macro that is generally used, where hygiene_var would be the socket.
Mech.get_var/0 is the macro to just obtain the hygiene_var.
My guess is that since the get_var macro is invoked in the do-block it is in some kind of second pass of macro expansion where the hygiene_var argument of the def has already lost its context or something.
How do I do this? Should I just var! it and always leak it into the scope?
If you are not sure then simply look at the documentation: Macro hygiene | Macros @ Elixir documentation. Keep in mind that in Elixir you can debug/inspect anything including the result of quote do … end special form call. This should be enough hint for you to proceed.
I’m not really sure about your macros. There is too much “magic” and I would instead propose to create something like:
defmodule Test do
import Mech
foo %{assign_name: assigned_value} do
assigned_value <> " bar"
end
end
This should be much simpler to implement and also would be much more clear for the readers.
FWIW, you can use var! with its second context argument to avoid leaking:
defmacro foo(do: block) do
quote do
def fun(var!(hygiene_var, __MODULE__)) do
unquote(block)
end
end
end
defmacro get_var do
quote do
var!(hygiene_var, __MODULE__)
end
end
This allows your Test module to compile, but explicitly sayinghygiene_var directly inside of the foo do block will still fail to compile.
When choosing var!/2 I would recommend to pass unquote(__MODULE__) instead as this way you would have no problem with other macros like the one you writing.