0
votes

So in the docs for defmacro in Elixir we have (I put in a hello method for example purposes):

defmodule MyLogic do
  defmacro unless(expr, opts) do
    quote do
      if !unquote(expr), unquote(opts)
    end
  end

  def hello, do: "hello"
end

Then if I try to introspect on available functions, only hello/0 shows.

iex(3)> MyLogic.__info__(:functions)
[hello: 0]

But calling:

MyLogic.unless false do
  IO.puts("It works")
end

does invoke the macro.

My understanding is that the macro is expanded into final AST form and there is no trace of it once it is compiled. What I'm finding a bit confusing is that it seems you can invoke unless like a function but there's no trace of it in the introspection .__info__(:functions).

1) I think you cannot introspect on macro definitions post-compilation because there's no trace of it after compilation, but in building a mental model of "unless" after it is expanded, how should I represent it in my mind? Should I just look at it as "MyLogic" now has this atom name unless that invokes the result of the if expression if !unquote(expr), unquote(opts).
2) And how does Elixir know then that unless is a shortcut to the fully expanded macro code?

2

2 Answers

3
votes

According to the documentation, the :functions atom is used to retrieve the functions of a module only.

If you want to get the macros instead, you should supply the atom :macros to __info__/1 instead.

Since you can't have a macro and a function that share both the same name and arity, this makes it easy for Elixir to distinguish between the two.

0
votes

@TGO already gave a perfect answer on why __info__(:functions) does not show unless; I still think there is some clarification required.


My understanding is that the macro is expanded into final AST form and there is no trace of it once it is compiled.

That is indeed very true. The thing is Elixir is compiling everything before execution, and by writing MyLogic.unless ... you by no mean call/invoke the macro. What happens, you instruct Elixir to inject the AST returned by MyLogic.unless into your code instead of MyLogic.unless, during compilation of your code. There is no trace of the macro in the resulting BEAM.

how should I represent it in my mind?

MyLogic.unless false, do: IO.puts("It works")

is expanded exactly into:

if !false, do: IO.puts("It works")

unquote is necessary there in macro declaration under quote do call because macros do receive quoted expressions as arguments. You might try to IO.inspect/2 arguments inside a call to macro, outside of the quote do:

defmacro unless(expr, opts) do
  IO.inspect({expr, opts}, label: "compilation time!")
  quote, do: if !unquote(expr), unquote(opts)
end

how does Elixir know then that unless is a shortcut to the fully expanded macro code?

There is a truly remarkable explanation of how Elixir maintains internal info on compilation stage which this margin is too small to contain. In general, Elixir does not know anything. You should clearly distinguish compilation time and runtime contexts. You are talking about execution. That happens after everything is compiled and your custom MyLogic.unless/1 does not exist anymore, substituted by the AST injected.

Elixir cannot execute not compiled code, it’s not a scripting language. Don’t get fooled by REPL: iex under the hood effectively compiles everything you “execute” there before real execution.

that a macro in Elixir is actually a function (from your comments to another answer)

This is by no means correct. Functions do exist as code that gets executed by ErlangVM in the resulting BEAM (after compilation passed.) Macros do not.