Reputation: 11689
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
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