When we extract the internal representation of some code (either via a macro parameter or using quote), we stop Elixir from adding it automatically to the tuples of code it is building during compilation—we’ve effectively created a free-standing island of code. How do we inject that code back into our program’s internal representation?
There are two ways.
The first is our old friend the macro. Just like with a function, the value a macro returns is the last expression evaluated in that macro. That expression is expected to be a fragment of code in Elixir’s internal representation. But Elixir does not return this representation to the code that invoked the macro. Instead it injects the code back into the internal representation of our program and returns to the caller the result of executing that code. But that execution takes place only if needed.
We can demonstrate this in two steps. First, here’s a macro that simply returns its parameter (after printing it out). The code we give it when we invoke the macro is passed as an internal representation, and when the macro returns that code, that representation is injected back into the compile tree.
| defmodule My do |
| defmacro macro(code) do |
| IO.inspect code |
| code |
| end |
| end |
| defmodule Test do |
| require My |
| My.macro(IO.puts("hello")) |
| end |
When we run this, we see
| {{:.,[line: 11],[{:__aliases__,[line: 11],[:IO]},:puts]}, [line: 11],["hello"]} |
| hello |
Now we’ll change that file to return a different piece of code. We use quote to generate the internal form:
| defmodule My do |
| defmacro macro(code) do |
| IO.inspect code |
| quote do: IO.puts "Different code" |
| end |
| end |
| defmodule Test do |
| require My |
| My.macro(IO.puts("hello")) |
| end |
This generates
| {{:.,[line: 11],[{:__aliases__,[line: 11],[:IO]},:puts]}, [line: 11],["hello"]} |
| Different code |
Even though we passed IO.puts("hello") as a parameter, it was never executed by Elixir. Instead, it ran the code fragment we returned using quote.
Before we can write our version of if, we need one more trick—the ability to substitute existing code into a quoted block. There are two ways of doing this: by using the unquote function and with bindings.
Let’s get two things out of the way. First, we can use unquote only inside a quote block. Second, unquote is a silly name. It should really be something like inject_code_fragment.
Let’s see why we need this. Here’s a simple macro that tries to output the result of evaluating the code we pass it:
| defmacro macro(code) do |
| quote do |
| IO.inspect(code) |
| end |
| end |
Unfortunately, when we run it, it reports an error:
| ** (CompileError).../eg2.ex:11: function code/0 undefined |
Inside the quote block, Elixir is just parsing regular code, so the name code is inserted literally into the code fragment it returns. But we don’t want that. We want Elixir to insert the evaluation of the code we pass in. And that’s where we use unquote. It temporarily turns off quoting and simply injects a code fragment into the sequence of code being returned by quote.
| defmodule My do |
| defmacro macro(code) do |
| quote do |
| IO.inspect(unquote(code)) |
| end |
| end |
| end |
Inside the quote block, Elixir is busy parsing the code and generating its internal representation. But when it hits the unquote, it stops parsing and simply copies the code parameter into the generated code. After unquote, it goes back to regular parsing.
There’s another way of thinking about this. Using unquote inside a quote is a way of deferring the execution of the unquoted code. It doesn’t run when the quote block is parsed. Instead it runs when the code generated by the quote block is executed.
Or, we can think in terms of our quote-as-string-literal analogy. In this case, we can make a (slightly tenuous) case that unquote is a little like the interpolation we can do in strings. When we write "sum=#{1+2}", Elixir evaluates 1+2 and interpolates the result into the quoted string. When we write quote do: def unquote(name) do end, Elixir interpolates the contents of name into the code representation it is building as part of the list.
Consider this code:
| iex> Code.eval_quoted(quote do: [1,2,unquote([3,4])]) |
| {[1,2,[3,4]],[]} |
The list [3,4] is inserted, as a list, into the overall quoted list, resulting in [1,2,[3,4]].
If we instead wanted to insert just the elements of the list, we could use unquote_splicing.
| iex> Code.eval_quoted(quote do: [1,2,unquote_splicing([3,4])]) |
| {[1,2,3,4],[]} |
Remembering that single-quoted strings are lists of characters, this means we can write
| iex> Code.eval_quoted(quote do: [?a, ?= ,unquote_splicing('1234')]) |
| {'a=1234',[]} |
We now have everything we need to implement an if macro.
| defmodule My do |
| defmacro if(condition, clauses) do |
| do_clause = Keyword.get(clauses, :do, nil) |
| else_clause = Keyword.get(clauses, :else, nil) |
| quote do |
| case unquote(condition) do |
| val when val in [false, nil] -> unquote(else_clause) |
| _ -> unquote(do_clause) |
| end |
| end |
| end |
| end |
| defmodule Test do |
| require My |
| My.if 1==2 do |
| IO.puts "1 == 2" |
| else |
| IO.puts "1 != 2" |
| end |
| end |
It’s worth studying this code.
The if macro receives a condition and a keyword list. The condition and any entries in the keyword list are passed as code fragments.
The macro extracts the do: and/or else: clauses from that list. It is then ready to generate the code for our if statement, so it opens a quote block. That block contains an Elixir case expression. This case expression has to evaluate the condition that is passed in, so it uses unquote to inject that condition’s code as its parameter.
When Elixir executes this case statement, it evaluates the condition. At that point, case will match the first clause if the result is nil or false; otherwise it matches the second clause. When a clause matches (and only then), we want to execute the code that was passed in either the do: or else: values in the keyword list, so we use unquote again to inject that code into the case.