Using the Representation as Code

Understand how to inject our code into a program's internal representation.

Introduction

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’s 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 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 doesn’t 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 the result of executing that code to the caller. But that execution takes place only if needed.

We can demonstrate this in two steps.

  1. First, here’s a macro that simply returns its parameter (after printing it out).
  2. When we invoke the macro, the code is passed as an internal representation. The macro returns that code and 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

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 the following:

{{:.,[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 the ability to substitute existing code into a quoted block. There are two ways of doing this: by using the unquote function and by using bindings.

The unquote function

Let’s get two things out of the way.

  • First, we can use unquote only inside a quote block.
  • Second, unquote is a misleading 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. We can make a 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’s building as part of the list.

Expanding a list: unquote_splicing

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],[]}

Remember that single-quoted strings are lists of characters, this means we can write the following:

iex> Code.eval_quoted(quote do: [?a, ?= ,unquote_splicing('1234')]) 
{'a=1234',[]}

Back to our myif macro

We now have everything we need to implement an if macro.

Get hands-on with 1200+ tech skills courses.