Elixir var! ... How to read a variable from caller's scope

958 views Asked by At

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

3

There are 3 answers

1
Aleksei Matiushkin On BEST ANSWER

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.

0
Everett On

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.

3
Peaceful James On

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.