wfgilman

wfgilman

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

Marked As Solved

wfgilman

wfgilman

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!

Also Liked

OvermindDL1

OvermindDL1

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:

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

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?

OvermindDL1

OvermindDL1

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. ^.^

wfgilman

wfgilman

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.

wfgilman

wfgilman

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

Where Next?

Popular in Questions Top

LegitStack
I’m hoping you guys can give me some general advice and perhaps code examples if you’re feeling up to it. I’m very interested in Elixir,...
New
Kagamiiiii
Student &amp; New to elixir. Nice language. I want to convert a english character, e.g. “a”, which is stored in a variable, to it’s asci...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 35421 110
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New

We're in Beta

About us Mission Statement