Macro expansion in elixir: how to define 2 macros

2020-08-27 05:34发布

问题:

I am experimenting with Macros in elixir. Therefore, the code I'm about to show should certainly be done with simple functions but.. I'm experimenting !

I want to define 2 macros (A and B) and make A use B to experiment with macro expansion. When I use A, I get a compile error saying that the function B is undefined.

Here is the code:

defmodule MyMacros do
  defmacro print_expr(expr) do
    quote do
      IO.puts(unquote(expr))
    end
  end

  defmacro print_hashes_around(expr) do
    quote do
      IO.puts "###"
      print_expr(unquote(expr))
      IO.puts "###"
    end
  end
end

defmodule MyModule do
  require MyMacros

  def my_print(expr) do
    MyMacros.print_hashes_around(expr)
  end
end

MyModule.my_print("hello world")

And here is the compile error:

macro_test.exs:17: warning: redefining module MyModule
** (CompileError) macro_test.exs:21: function print_expr/1 undefined
(stdlib) lists.erl:1336: :lists.foreach/2
macro_test.exs:17: (file)
(elixir) lib/code.ex:307: Code.require_file/2

The way I (mis)understand things:

  1. By requiring MyMacros, the module MyModule should know the existence of both macros. Therefore I should be able to use any macros.
  2. When print_hashes_around is expanded in MyModule, the compiler should find that print_expr is also a macro. Therefore, another expansion should happen.
  3. What seems to happen is that second expansion does not happen. Therefore the compiler looks for a function definition that does not exist.

Am I right ?

As suggested in slack, prefixing print_expr with MyMacros. fixes it. I still don't understand why. MyModule requires MyMacros so both Macros should be known and expandable... When I look at the definition of unless, it uses if, not Kernel.if.

回答1:

By requiring MyMacros, the module MyModule should know the existence of both macros. Therefore I should be able to use any macros.

The misunderstanding is here. :) require only makes the module available to the compiler, it does not import the module functions. If you used import MyModule then it would work.

However, it would be best to fix the issue by prefixing the module name, because then you allow developers using your code to use your macros explicitly (with require) or by importing them.

Another option is to avoid multiple macro invocations like this:

defmodule MyMacros do
  defmacro print_expr(expr) do
    quoted_print_expr(expr)
  end

  defmacro print_hashes_around(expr) do
    quote do
      IO.puts "###"
      unquote(quoted_print_expr(expr))
      IO.puts "###"
    end
  end

  defp quoted_print_expr(expr) do
    quote do
      IO.puts(unquote(expr))
    end
  end
end


标签: macros elixir