Francesco Belladonna
Francesco Belladonna

Reputation: 11689

Invoke a private macro within a quote block

I'm trying to invoke a private macro, within a quote block, using a variable defined within the code block itself. This is the pseudo-code showing what I would like to do (doesn't work)

defmodule Foo do
  defmacrop debug(msg) do
    quote bind_quoted: [msg: msg], do: IO.puts(msg)
  end

  defmacro __using__(_) do
    quote do
      def hello do
        my = "testme"

        unquote(debug(quote do: my))
      end
    end
  end
end

defmodule Bar do
  use Foo
end

Bar.hello()

And this would get converted (in my mind), at compile time to:

defmodule Bar do
  def hello do
    my = "testme"
    IO.puts(my)
  end
end

Is there any way to achieve this? I'm struggling to find any documentation related to it.

Update

I discovered that:

defmodule Foo do
  defmacrop debug() do
    quote do: IO.puts("hello")
  end

  defmacro __using__(_) do
    quote do
      def hello do
        my = "testme"

        unquote(debug())
      end
    end
  end
end

Gets properly converted to what I need, but I'm struggling find a way to pass the variable as is, so that it becomes IO.puts(my)

Upvotes: 3

Views: 115

Answers (1)

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

The issue here is with nested quoting: the private macro should return the double-quoted expression (since to invoke it from the outer scope one needs to explicitly unquote, and macro is still expected to return a quoted expression.)

Sidenote: your update section is wrong; you might notice, that "hello" is printed during a compilation stage, namely when use Foo is being compiled. That is because the double-quoting is needed, the code in your update section executes IO.puts when unquote in __using__ macro is met.

On the other hand, my should be quoted only once. That might be achieved with an explicit quoting of AST, passing the msg there as is:

defmodule Foo do
  defmacrop debug(msg) do
    quote bind_quoted: [msg: msg] do
      {
        {:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]},
        [],
        [msg]} # ⇐ HERE `msg` is the untouched argument
    end 
  end 

  defmacro __using__(_) do
    quote do
      def hello do
        my = "testme"

        unquote(debug(quote do: my))
      end 
    end 
  end 
end

defmodule Bar do
  use Foo 
end

Bar.hello()
#⇒ "testme"

I was unable to achieve the same functionality with options in the call to Kernel.SpecialForms.quote/2; the only available related option is unquote to tune the unquoting inside nested quotes, while we need the exact opposite.


Sidenote: below does not work and I expect this to be a bug in Kernel.SpecialForms.quote/2 implementation.

quote bind_quoted: [msg: msg] do
  quote bind_quoted: [msg: msg], do: IO.puts(msg)
end

FWIW: I filed an issue.

I believe it might be a good feature request to Elixir core, to allow an option that disables additional quoting.


Sidenote 2: the following works (most concise approach):

defmacrop debug(msg) do
  quote bind_quoted: [msg: msg] do
    quote do: IO.puts(unquote msg)
  end
end

So you might avoid tackling with an explicit AST and just use the above. I am leaving the answer as is, since dealing with AST directly is also a very good option, that should be used as a sledgehammer / last resort, which does always work.


If IO.puts is not your desired target, you might call quote do: YOUR_EXPR on what you want to have in debug macro:

quote do: to_string(arg)
#⇒ {:to_string, [context: Elixir, import: Kernel], [{:arg, [], Elixir}]}

and manually unquote the arg in the result:

#                                             ✗  ⇓⇓⇓ {:arg, [], Elixir} 
#                                             ✓  ⇓⇓⇓ arg
{:to_string, [context: Elixir, import: Kernel], [arg]}

This is basically how I got the AST of your original request (IO.puts.)

Upvotes: 1

Related Questions