Improving macros usability #306

Closed
josevalim opened this Issue May 26, 2012 · 4 comments

Projects

None yet

2 participants

@josevalim
Member

This document is an attempt to discuss and propose improvements to Elixir's current macro system and meta-programming capabilities.

Quoting and stacktraces

Today, whenever you quote an expression, Elixir returns 0 as line:

iex> quote do: foo()
{:foo,0,[]}

This allows us to apply the proper lines when a macro is invoked. For example, consider the test macro that ships with ExUnit.Case. A simplistic implementation for it would be:

defmacro test(name, expr) do
  quote do
    def unquote(:"test_#{name}"), unquote(expr)
  end
end

Which would then be called:

test :foo do
  bar()
end

In such cases, the quote would return 0 as line for each expression defined inside the quote but the unquoted expressions would keep their original lines. For example, the macro would return:

{ :def, 0, [:test_foo, [do: { :bar, 2, [] }]] }

When the quote is being expanded by the macro, the lines equal to 0 are changed to the call site, so the contents above are translated to (assuming test was called on line 1):

{ :def, 1, [:test_foo, [do: { :bar, 2, [] }]] }

So the function definition ends up in exactly the same line we called the test macro, which is exactly what we want! Keep this in mind, it will be important in the next section.

Environment information

The great and horrible thing about macros is that it receives a representation of its arguments but not its values. Our test macro above is restricted to support only atoms as arguments. Let's suppose we want to dynamically define a test:

test_name = :foo
test test_name do
  assert true
end

This will not work because our macro will not receive the value of test_name, but its representation:

{ test_name, 2, nil }

And Elixir will raise an error when we interpolate the tuple inside the atom. This means we need to evaluate the macro contents before generating the function, for example:

defmacro test(name, expr) do
  quote do
    name = :"test_#{unquote(name)}"
    def name, unquote(expr)
  end
end

But this doesn't work, because now it will always define a function named name. The best approach would be to evaluate the contents and call an internal method, for example:

defmacro test(name, expr) do
  quote do
    ExUnit.Case.define_test(__MODULE__, __FILE__, __LINE__, unquote(name), quote do: unquote(expr))
  end
end

def define_test(module, file, line, name, expr) do
  contents = quote do
    def unquote(:"test_#{name}"), unquote(expr)
  end
  Module.eval_quoted __MODULE__, contents, [], file: file, line: line
end

However, this solution is much more verbose than I would expect. We need to pass module, file and line information so it keeps the same characteristics as the previous section (i.e. the file and line in stacktrace will be kept the same as the call site). Therefore, I would like to propose an __ENV__ variable that would contain all this information (and potentially more in the future), allowing us to write:

defmacro test(name, expr) do
  quote do
    ExUnit.Case.define_test(__ENV__, unquote(name), quote do: unquote(expr))
  end
end

For macros, we could always pass the __ENV__ implicitly, so all this information is already available upfront. The __ENV__ variable holds compile time information, so it doesn't make sense to have it for normal functions. That said:

  1. What do you think about __ENV__ and automatically passing __ENV__ to macros?

  2. Do we still need __MODULE__, __FILE__, __LINE__ and friends considering __ENV__ will be available? The idea is that you will be able to get those from __ENV__ as __ENV__.module and so forth. I don't know the answer to this question right now, I will have a better idea after I add __ENV__ and use it in the source, but if someone has a good reason in advance to not deprecate the old ones, please speak up!

That's it, thanks for reading. :) /cc @alco @rafaelfranca

@rafaelfranca
Contributor

About 1): will this add any drawback?

About 2): none good reason to not deprecate.

@josevalim
Member

About 1): there aren't any drawbacks. I even want to keep this structure the same as used internally by Elixir but providing "accessors" just to some of the data. This will help us write some macros that are currently in Erlang in Elixir and should not affect performance.

@rafaelfranca
Contributor

So. Let's move forward with this.

@josevalim
Member

Pushed this to master. I have deprecated __LINE__ and __FUNCTION__, I kept __FILE__ and __MODULE__ because they are frequently used. I have also added __ENV__ which returns the line, function and more information.

Macros also have access to the caller environment in __CALLER__. This should simplify some macros and also make them more powerful, for example, I am rewriting a couple macros that were in Erlang in pure Elixir.

@josevalim josevalim closed this May 27, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment