Help with implementing a macro

I’m trying to implement a conceptual test with the following macro. I want to be able to pass an action argument which will call a function of the same name in another module with the msg macro argument as the callback function’s argument. This is the same design as the instrument/3 macro in Phoenix.Endpoint.Instrument. But I’m getting an error when I try to compile it.

Can anyone help me understand why?

Error:
warning: variable "action" does not exist and is being expanded to "action()", please use parentheses to remove the ambiguity or change the variable name
  macro.ex:11


== Compilation error on file macro.ex ==
** (CompileError) macro.ex:11: undefined function action/0
    (elixir) expanding macro: Kernel.def/2
    macro.ex:10: MacroTest (module)
    (elixir) lib/kernel/parallel_compiler.ex:117: anonymous fn/4 in Kernel.ParallelCompiler.spawn_compilers/1

** (exit) shutdown: 1
    (elixir) lib/kernel/parallel_compiler.ex:291: Kernel.ParallelCompiler.handle_failure/3
    (elixir) lib/kernel/parallel_compiler.ex:247: Kernel.ParallelCompiler.wait_for_messages/1
    (elixir) lib/kernel/parallel_compiler.ex:62: Kernel.ParallelCompiler.spawn_compilers/3
       (iex) lib/iex/helpers.ex:170: IEx.Helpers.c/2

The code:

defmodule MacroTest do

  defmacro my_macro(action, msg) do
    sender = " from #{__MODULE__}"
    quote do
      unquote(__MODULE__).my_fun(unquote(action), unquote(msg), unquote(sender))
    end
  end

  def my_fun(action, var!(msg), var!(sender)) do
    unquote(MacroTest.run_action(action))
  end

  def run_action(action) do
    quote do
      MyOtherModule.unquote(action)(var!(msg), var!(sender))
    end
  end

end

defmodule MyOtherModule do

  def sender(msg, sender) do
    send self(), msg <> sender
  end

  def printer(msg, sender) do
    IO.puts msg <> sender
  end

end
1 Like

I’m not entirely clear on what you’re trying to accomplish here, but you have at least 2 major difficulties with the code as is:

  1. You can’t use a macro from the same module that defines the macro.
  2. You can’t use var! out side of a quoted context.

Can you provide an example of what using this macro looks like, and what code you’re hoping the macro would produce?

2 Likes

I’d like it to work the following way:

iex> MacroTest.my_macro(:sender, "hi")
"hi from MacroTest"

iex> MacroTest.my_macro(:printer, "hi")
hi from MacroTest
:ok

I’m trying to replicate a conceptual example of the instrument/3 macro. I just can’t seem to reproduce it due to my lack of understanding of macros.

I thought my_fun/3 was being called in the quoted context for example, because it is being called from within the macro definition.

1 Like

Hmm, let’s see…

Nope, it is not being called from the quoted context, it is being passed as a call, and since it is defined as:

So it is not a macro, so it gets stored into the AST as a call for run-time, also the rest of the head:

Uhh, does var! even work there? I guess it might but it would be basically no-op in that position since you are naming a variable that is being defined in a head anyway, those should only ‘only’ ever be used in a quoted context. ^.^

2 Likes

Okay, I can see that I have no business writing macros at my level of understanding since I can’t even follow these responses :joy:. I was trying to write a module for testing that simulates the instrument/3 macro in Phoenix.Endpoint.Instrument so I can test my instrumenters. The macro should produce the following code:

  def instrument(:my_event, compile, runtime, fun) do
    result = MyModule.my_event(:start, compile, runtime)
 
    start = :erlang.monotonic_time()
    try do
      fun.()
    after
      diff = :erlang.monotonic_time() - start
      MyModule.my_event(:stop, diff, result)
    end
  end
1 Like

Nah, it’s just practice. ^.^

Just remember, if a function is defined as defmacro(p) then it gets passed the AST of the arguments and should return an AST. If a function is defined as def(p) then it is executed at run-time, I.E. in the run context of where it is called instead of when its context is defined.

Now given that, you call your my_macro/2, it is a defmacro so it is executed at the build-time of the context it is called from. Inside of it you define a binding named sender of that string, that line is executed in the run context of my_macro/2 inside the MacroTest module. The next expression, the quote creates a new context inside of it, the code you define is not doing anything, not calling macro’s, not binding anything, it does nothing beyond just define the appropriate AST. The quote call overrides a few special things, like unquote/1 and var!/1. The unquote/1 just runs whatever code is inside of it and injects it into the AST at its point, so doing this:

iex(1)> a = Macro.var(:blah, __MODULE__)
{:blah, [], nil}
iex(2)> quote(do: unquote(a) = 42)
{:=, [], [{:blah, [], nil}, 42]}

Which is entirely identical to:

iex(3)> a = Macro.var(:blah, __MODULE__)
{:blah, [], nil}
iex(4)> {:=, [], [a, 42]}
{:=, [], [{:blah, [], nil}, 42]}

It is just like putting the variable directly into the AST itself, just inside of a quote context to make it easier.

Now, unquote/1 is a special form, it just *can*not* be called out of a quoted context, and in fact you get the error of (CompileError) unquote called outside quote, however var!/1 is just a macro, all it does is resolve the given variable and return as in:

iex(5)> a = Macro.var(:blah, __MODULE__)
{:blah, [], nil}
iex(6)> var!(a)
{:blah, [], nil}

Or when in a head, like in your my_fun/3, it returns the binding, so it is the same as not having it there at all (I’m not actually sure why you put var!/1 in the arguments at all, unsure what that was supposed to do). In essence, it basically does nothing most of the time, however it is useful at times to generate AST, so it is useful in, say, a quote (which will end up just returning the given variable verbatim with no context). Now do note, a binding in the ast is just {name, meta, context}, the name is an atom, the meta is a keyword list of meta information, like line number, the empty list of [] is fine, and the context is an atom. The name is obviously the name, how it is referred to. The context is just anything to define a ‘uniqueness’ to the name, so a binding of {:blah, [], nil} and {:blah, [], Blah} are two different bindings, even though they have the same name, they are two different bindings. A variable in a quote is always given a default context, so doing this:

iex(7)> quote(do: blah = 42)
{:=, [], [{:blah, [], Elixir}, 42]}

As you can see it gave it a context, Elixir in this case since I was in iex, but usually your module, that just makes it so your variables do not accidentally stomp all over the variables in where-ever you are injecting this AST in to, var!/1 is just a convenient way to refer to some variable in the global context of where this is going to be injected, which always has a nil context.

Now in your quote you are unquote’ing your __MODULE__, which injects the module name ast into the ast you are building, which you then call (via . dot) the my_fun/3function, which will have 3 arguments, each of which are unquoted in to the ast. In your second post you have examples like MacroTest.my_macro(:printer, "hi"), so ‘action’ is becoming the ast of the atom :printer, which will actually just be :printer nicely enough, and same of msg with "hi", so that the quote expression puts out is basically MacroTest.my_fun(:printer, "hi", "from MacroTest"), and that is injected into the context that is being built in where-ever my_macro/2 is being called from, so it will then process that just like normal ast, it sees that my_fun/3 is not a macro so it leaves the call in the ast and stores it for the run-time phase later (after compiling and so forth).

Later when the context that called my_macro/2 is ‘run’ (or ‘now’ if in iex for example), then my_fun/3 will be called as above. What happens is that my_fun calls unquote(MacroTest.run_action(action)), and it will die horrible with the above error message if unquote gets called, so no matter what happens here it dies. :slight_smile:

Just remember the two contexts. :wink:

For note, you probably want to do something like:

defmodule MacroTest do
  defmacro my_macro(action, msg) do
    sender = " from #{__MODULE__}"
    quote do
      MyOtherModule.unquote(action)(unquote(msg), unquote(sender))
    end
  end
end

That will have your examples above work. :slight_smile:

I’ve no clue about the instrument part in your latest message, I’ve never actually used phoenix’s instrumentation (I’m used to erlang’s lower level facilities) so unsure what needs to be what or how. If you could give a full example of the test of what you want the api to be and how it should work to test then I’m sure someone here could help. :slight_smile:

3 Likes

I wish I could give multiple likes! Thank you for the detailed response and education. It helped me solve my problem :blush:

Below is a working version of my macro. I am dropping this in my test module to simulate Phoenix’s Instrumentation API so I can test my instrumenter:

defmodule Phoenix.Endpoint do

  defmacro instrument(event, runtime \\ Macro.escape(%{}), fun) do
    compile = Macro.escape(strip_caller(__ENV__))

    quote do
      result = NewRelix.Instrumenter.unquote(event)(:start, unquote(compile),
        unquote(runtime))
      start = :erlang.monotonic_time()
      try do
        unquote(fun).()
      after
        diff = :erlang.monotonic_time() - start
        NewRelix.Instrumenter.unquote(event)(:stop, diff, result)
      end
    end
  end

  defp strip_caller(%Macro.Env{module: mod, function: fun, file: file,
                                                            line: line}) do
    %{module: mod, function: form_fa(fun), file: file, line: line}
  end

  defp form_fa({name, arity}) do
    Atom.to_string(name) <> "." <> Integer.to_string(arity)
  end
  defp form_fa(nil), do: nil
end

I can then use it in my test like-so:

require __MODULE__.Phoenix.Endpoint

Phoenix.Endpoint.instrument :phoenix_controller_call, %Plug.Conn{}, fn ->
  :timer.sleep(500)
end

Also helpful was @sasajuric’s series on macros. This community is awesome!

3 Likes