Macros Inject Code

Let’s pretend we’re the Elixir compiler. We read a module’s source top to bottom and generate a representation of the code we find. That representation is a nested Elixir tuple.

If we want to support macros, we need a way to tell the compiler that we’d like to manipulate a part of that tuple. We do that using defmacro, quote, and unquote.

In the same way that def defines a function, defmacro defines a macro. You’ll see what that looks like shortly. However, the real magic starts not when we define a macro, but when we use one.

When we pass parameters to a macro, Elixir doesn’t evaluate them. Instead, it passes them as tuples representing their code. We can examine this behavior using a simple macro definition that prints out its parameter.

macros/dumper.exs
 defmodule​ My ​do
 defmacro​ macro(param) ​do
  IO.inspect param
 end
 end
 
 defmodule​ Test ​do
 require​ My
 
 # These values represent themselves
  My.macro ​:atom​ ​#=> :atom
  My.macro 1 ​#=> 1
  My.macro 1.0 ​#=> 1.0
  My.macro [1,2,3] ​#=> [1,2,3]
  My.macro ​"​​binaries"​ ​#=> "binaries"
  My.macro { 1, 2 } ​#=> {1,2}
  My.macro ​do​: 1 ​#=> [do: 1]
 
 # And these are represented by 3-element tuples
 
  My.macro { 1,2,3,4,5 }
 # => {:"{}",[line: 20],[1,2,3,4,5]}
 
  My.macro ​do​: ( a = 1; a+a )
 # => [do:
 # {:__block__,[],
 # [{:=,[line: 22],[{:a,[line: 22],nil},1]},
 # {:+,[line: 22],[{:a,[line: 22],nil},{:a,[line: 22],nil}]}]}]
 
 
  My.macro ​do
  1+2
 else
  3+4
 end
 # => [do: {:+,[line: 24],[1,2]},
 # else: {:+,[line: 26],[3,4]}]
 
 end

This shows us that atoms, numbers, lists (including keyword lists), binaries, and tuples with two elements are represented internally as themselves. All other Elixir code is represented by a three-element tuple. Right now, the internals of that representation aren’t important.

Load Order

You may be wondering about the structure of the preceding code. We put the macro definition in one module, and the usage of that macro in another. And that second module included a require call.

Macros are expanded before a program executes, so the macro defined in one module must be available as Elixir is compiling another module that uses those macros. The require function tells Elixir to ensure the named module is compiled before the current one. In practice it is used to make the macros defined in one module available in another.

But the reason for the two modules is less clear. It has to do with the fact that Elixir first compiles source files and then runs them.

If we have one module per source file and we reference a module in file A from file B, Elixir will load the module from A, and everything just works. But if we have a module and the code that uses it in the same file, and the module is defined in the same scope in which we use it, Elixir will not know to load the module’s code. We’ll get this error:

 ** (CompileError)
  .../dumper.ex:7:
  module My is not loaded but was defined. This happens because you
  are trying to use a module in the same context it is defined. Try
  defining the module outside the context that requires it.

By placing the code that uses the module My in a separate module, we force My to load.

The quote Function

We’ve seen that when we pass parameters to a macro they are not evaluated. The language comes with a function, quote, that also forces code to remain in its unevaluated form. quote takes a block and returns the internal representation of that block. We can play with it in IEx:

 iex>​ ​quote​ ​do​: ​:atom
 :atom
 iex>​ ​quote​ ​do​: 1
 1
 iex>​ ​quote​ ​do​: 1.0
 1.0
 iex>​ ​quote​ ​do​: [1,2,3]
 [1,2,3]
 iex>​ ​quote​ ​do​: ​"​​binaries"
 "binaries"
 iex>​ ​quote​ ​do​: {1,2}
 {1,2}
 iex>​ ​quote​ ​do​: [​do​: 1]
 [do: 1]
 iex>​ ​quote​ ​do​: {1,2,3,4,5}
 {:"{}",[],[1,2,3,4,5]}
 iex>​ ​quote​ ​do​: (a = 1; a + a)
 {:__block__, [],
  [{:=, [], [{:a, [], Elixir}, 1]},
  {:+, [context: Elixir, import: Kernel],
  [{:a, [], Elixir}, {:a, [], Elixir}]}]}
 iex>​ ​quote​ ​do​: [ ​do​: 1 + 2, ​else​: 3 + 4]
 [do: {:+, [context: Elixir, import: Kernel], [1, 2]},
  else: {:+, [context: Elixir, import: Kernel], [3, 4]}]

There’s another way to think about quote. When we write "abc", we create a binary containing a string. The double quotes say, “interpret what follows as a string of characters and return the appropriate representation.”

quote is the same: it says, “interpret the content of the block that follows as code, and return the internal representation.”