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.
| 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.
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.
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.”