Debugging A Broken Elixir Macro

Elixir has many unique features that make it interesting to learn. One of them is its ability to manupulate AST before a program is started, known as macros. But macros can be misleading as shown in the following snippet

Broken List Length Macro

Consider the following macro that "calculates" a list length:

defmacro broken_length(list) do
  val = length(list)
  quote do: unquote(val)
end

An elixir macro takes a quoted expression and returns a new quoted expression. Forgetting the input is also a quoted expression may lead one to assume the above returns a list length. That assumption may even be proved using some test code:

iex(2)> HelloWorld.broken_length([1,2,3])
3

But it should soon break when real lists are passed in:

l = [1,2,3]
iex(2)> HelloWorld.broken_length(l)
** (ArgumentError) argument error
    :erlang.length({:g, [line: 4], nil})
    (hello_world) expanding macro: HelloWorld.broken_length/1
    iex:4: (file)

What Went Wrong?

As noted a macro takes a quoted expression and its job is to transform it to another quoted expression. Here are the two quoted expressions we passed to the macro:

# Worked: A list
iex(4)> quote do: [1,2,3]
[1, 2, 3]

# Failed: A variable holding a list
iex(5)> quote do: g
{:g, [], Elixir}

Now I think the problem is obvious. A quoted list is the list, so calling length([1,2,3]) worked well. A quoted variable that holds a list is a tuple, and calling length({:g, [], Elixir}) is an error, as length expects a list.

A Simple Fix

Macros accept quoted expressions. All runtime calculation that has to be made, for example calculating the list size, has to appear inside the quoted return value.

The quoted return value represents the only "real" code that will get executed. Everything else happens at compile time and so happens too early.

Keeping that in mind it's easy to see the suggested macro can't work: As it tries to calculate the list length at compile time, and return a constant value at runtime.

A better approach is to calculate the length in the returned quoted expression:

defmacro fixed_length(list) do
  quote do: length(unquote(list))
end

More About Macros

Macros are an essential part in elixir so it's important to understand how they work. The series Understanding Elixir Macros from The Erlangelist has really helped me with understanding them.

Comments