Troubles with hygiene when calling macros from do block of other macro

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?

1 Like

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 saying hygiene_var directly inside of the foo do block will still fail to compile.

2 Likes

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.

I guess you are right, @Eiji. It’s a bit much magic.

I’ll make the final macro like this

defevent event_name(param_var :: ParamType, socket) do
  stuff(param_var)
end

where socket is optional.