Reputation: 3
Sorry if this has already been asked. Searching the forum for var!
gives me all the posts with word var
. Made it hard to narrow it down.
Struggling with writing a macro that reads a variable out of the caller's context and returns it from a function. Here's the simplest form of the problem I could think of:
defmodule MyUnhygienicMacros do
defmacro create_get_function do
quote do
def get_my_var do
var!(my_var)
end
end
end
end
defmodule Caller do
require MyUnhygienicMacros
my_var = "happy"
MyUnhygienicMacros.create_get_function()
end
The goal would be to see this when I run an iex session:
$ Caller.get_my_var()
"happy"
But this does not compile. The caller's my_var
goes unused too.
The CompileError expected "my_var" to expand to an existing variable or be part of a match.
I've read McCord's metaprogramming book, this blog post (https://www.theerlangelist.com/article/macros_6) and many others. Seems like it should work, but I just can't figure out why it won't..
Upvotes: 0
Views: 1259
Reputation: 121010
Kernel.var!/2
macro does not do what you think it does.
The sole purpose of var!/2
is to mark the variable off the macro hygiene. That means, using var!/2
one might change the value of the variable in the outer (in regard to the current context) scope. In your example, there are two scopes (defmacro[create_get_function]
and def[get_my_var]
) to bypass, which is why my_var
does not get through.
The whole issue looks like an XY-Problem. It looks like you want to declare kinda compile-time variable and modify it all way through the module code. For this purpose we have module attributes with accumulate: true
.
If you want to simply use this variable in create_get_function/0
, just unquote/1
it. If you want to accumulate the value, use module attributes. If you still ultimately want to keep it your way, passing the local compile-time variable through, break hygiene twice, for both scopes.
defmodule MyUnhygienicMacros do
defmacro create_get_function do
quote do
my_var_inner = var!(my_var)
def get_my_var, do: var!(my_var_inner) = 42
var!(my_var) = my_var_inner
end
end
end
defmodule Caller do
require MyUnhygienicMacros
my_var = "happy"
MyUnhygienicMacros.create_get_function()
IO.inspect(my_var, label: "modified?")
end
Please note, that unlike you might have expected, the code above still prints modified?: "happy"
during compile-time. This happens because var!(my_var_inner) = 42
call would be held until runtime, and bypassing macro hygiene here would be a no-op.
Upvotes: 2
Reputation: 9618
Forgive me if you already understand what is idiomatic in Elixir and what might be going against the grain. I'm presenting this answer in the hopes that it targets the spirit of your question.
First, everything in Elixir is an assignment, so in most cases it is impossible to read variables outside of the scope where they were created. Probably the hardest thing to unlearn from my days doing OO programming was the simple pattern (which looks a lot like the code in your question):
# pseudo-code
x = "something"
foreach y in x {
x = "something new"
}
That type of structure does not work in Elixir -- you often need to use some map or reduce functions to accomplish the equivalent result. You may be able to get around this restriction by using a macro, but there would probably have to be a really good justification for it. So perhaps you should re-think why you need such a structure or you could at a minimum share the justification so it is clear to others reading your question.
Secondly, consider passing arguments to your macros when values are needed -- this helps keep the scope more obvious. You can make use of unquote
to access the them, e.g.
defmodule Foo do
defmacro __using__(opts) do
quote do
def get_thing(), do: unquote(opts[:thing])
end
end
end
So that
defmodule Bar do
use Foo, thing: "blort"
end
defmodule Glop do
use Foo, thing: "eesh"
end
would allow you to do things like this:
Bar.get_thing() |> IO.puts() # "blort"
Glop.get_thing() |> IO.puts() # "eesh"
I would conclude that doing anything too fancy or clever in your macros can make them difficult to debug and maintain. For what it's worth, I've usually found it best to keep the macros "thin" (like thin controllers in MVC apps): they work best in my experience when they hand off to another regular function somewhere else, e.g.
defmacro __using__(opts) do
quote do
def thing(x), do: Verbose.thing(unquote(opts[:arg1]), unquote(opts[:arg2]), x)
end
end
Hopefully some ideas there are useful to your situation.
Upvotes: 0
Reputation: 2233
Have a look at these docs: https://hexdocs.pm/elixir/1.12/Kernel.SpecialForms.html#quote/2
There are plenty of examples. The my_var
is defined just inside the quote
block but not inside the def
function inside the quote
block.
You could do something like this:
defmodule MyUnhygienicMacros do
defmacro create_get_function() do
quote do
@my_var var!(my_var)
def get_my_var do
@my_var
end
end
end
end
defmodule Caller do
require MyUnhygienicMacros
my_var = "happy"
MyUnhygienicMacros.create_get_function()
end
Caller.get_my_var()
|> IO.inspect()
and call var!
just inside the quote
block, assigning to module attribute @my_var
.
But I am not very good at metaprogramming and there is probably somebody else who could answer it better.
Upvotes: 0