myronmarston

myronmarston

How to access module attributes in a macro outside `quote`?

In a macro, can you access a module attribute outside quote?. I know about Module.get_attributes/2 but I can only get it to work inside quote. Outside quote, it always returns nil whenever I have tried.

Here’s an example:

defmodule MyMacros do
  defmacro my_macro() do
    module = __CALLER__.module
    Module.get_attribute(module, :foo) |> IO.inspect(label: "outside quote")

    quote do
      Module.get_attribute(unquote(module), :foo) |> IO.inspect(label: "with unquoted module")
      Module.get_attribute(__MODULE__, :foo) |> IO.inspect(label: "with __MODULE__")
    end
  end
end

defmodule UseMacros do
  require MyMacros

  @foo bar: 1
  MyMacros.my_macro()
end

(On a side note: I formatted this with normal indentation, but the forum seems to render it incorrectly for some reason…)

Here’s the output this produces:

outside quote: nil
with unquoted module: [bar: 1]
with __MODULE__: [bar: 1]

This demonstrates that __CALLER__.module is correctly getting the module, but when I try to use it outside quote to get the module attribute, I get back nil.

Any ideas why or if there’s any way to get this to work?

Most Liked

josevalim

josevalim

Creator of Elixir

This may be the source of confusion. Code in Elixir is not evaluated (executed line by line) except on IEx. It is always compiled and then executed. This means that, if you have three lines:

foo_macro()
bar_macro()
baz_macro()

First we expand foo_macro(), then we expand bar_macro(), and then baz_macro() and just then we execute their contents. This means that @attr :bar won’t be seen inside any macro because it has not been executed yet.

OvermindDL1

OvermindDL1

Because code inside a quote is not run, the AST of the code is returned instead:

iex> quote do
...> blah bleh dorp
...> end
{:blah, [], [{:bleh, [], [{:dorp, [], Elixir}]}]}

So when your module returns the quote’s return, it is just returning AST, which is then just put in place in the module at the location of the module call, it is not executed until runtime as it is just code at this point in AST form. :slight_smile:

Teeeechnically the IO.puts/1 there is put into the module’s AST, it gets executed at ‘run-time’, but the run time in this case is the module definition call, the defmodule/2 call at the top is actually a macro (in essence) that takes the body of the do as an AST and passes it to the elixir module compilation function, that function runs over the do body of the module definition and executes each expression in turn, so IO.puts/1 will print at this point, a def will call the function definition function that actually creates the function to be run (passing that function the def’s do block as an AST), etc… :slight_smile:

Remember that mix compile has a runtime of its own, and that is where the module and function and such definitions are actually executed. It is a bit more defined then how I represented it above, but that is the gist of it.

Discourse is standard markdown, so use code-fences:
```elixir
Properly formatted elixir code here
```
Turns in to:

Properly formatted elixir code here

But it did not work because your Module.put_attribute(__MODULE__, :foo, bar: 1) line is being set as a call into the already compiled module AST rather than being run before your macro is run because put_attribute/3 is a def, not a defmacro, if it were a defmacro then it would run as you expect:

:slight_smile:

Macro’s are a black art, no matter the language you use, they require that you understand not only the language well, but also ‘how’ it is compiled in pretty good detail. ^.^

OvermindDL1

OvermindDL1

The attributes do not really ‘exist’ at this time yet unless they are registered, thus what you could do is change require to use at the call site, and make a __using__ macro in your module that register_attribute/3's the names that are needed, then in your my_macro/0 you can use Module.get_attribute(__CALLER__.module, :blah_attribute_name) or so to get the value, I think. :slight_smile:

I think that put_attribute/3 might be required though, but you can wrap that up in another macro too if needed.

myronmarston

myronmarston

I’m aware of that. Conceptually, code in a quote in a macro gets injected into the call site, as I understand it. But code in a module definition is evaluated as part of compilation, it’s not clear to me why code injected into a module definition from a macro quote is evaluated at “runtime” when it is clearly evaluated as part of the compilation process.

Remember that mix compile has a runtime of its own, and that is where the module and function and such definitions are actually executed.

If that’s true, then how does this work?

defmodule Foo do
  Enum.each [foo: 1, bar: 2], fn {k, v} ->
    def unquote(k)(), do: unquote(v)
  end
end

This dynamically defines a Foo.foo/0 function and a Foo.bar/0 function. From what you’ve said, the Enum.each/2 is not evaluated at compile time, running instead at runtime. But within the enumeration we are defining functions. So apparently functions are being defined and compiled at runtime instead of compile time? If module function compilation happens outside of “compile time” then what does “compile time” as a concept even mean?

Does the distinction you’re making have to do with macro expansion phases? That makes more sense to me then compile time vs runtime (given you can use “runtime” code to define functions for compilation, the distinction doesn’t make sense to me at all), and it’s something I’ve seen referenced in some Elixir books and documentation.

Discourse is standard markdown, so use code-fences:

Yep. I’ve been doing that from the start (as it’s what I’m used to from years of doing it on GitHub). Still not rendering correctly for me, though :(.

But it did not work because your Module.put_attribute(__MODULE__, :foo, bar: 1) line is being set as a call into the already compiled module AST rather than being run before your macro is run because put_attribute/3 is a def, not a defmacro, if it were a defmacro then it would run as you expect:

So, translating this into the concept of macro expansion phases, it sounds like my macro is being expanded during an expansion phase that happens before whatever @attribute value expands into is evaluated (or before put_attribute/3 is evaluated, if I call that directly). Is that accurate?

@josevalim I think there’s a bit of a whole in the documentation regarding this stuff. I’ve read @chrismccord’s Metaprogramming Elixir and the Elixir docs regarding macros and quote and I don’t really think it provides the necessary concepts to understand this kind of issue (at least not for how my brain thinks, apparently). Are there some simple ways we can improve the docs regarding this? I’m thinking maybe a doc explaining macro expansion phases in more detail and how evaluation order may not match what you’d expect. And also maybe the docs on the Module attribute functions could mention this gotcha. It definitely surprised me.

Where Next?

Popular in Questions Top

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
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
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
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
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement