Using the Representation as Code
Understand how to inject our code into a program's internal representation.
We'll cover the following
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.
- First, here’s a macro that simply returns its parameter (after printing it out).
- 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 aquote
block. - Second,
unquote
is a misleading name. It should really be something likeinject_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 1400+ tech skills courses.