Yos Riady software craftsman 🌱

Syntactic Extension with Elixir Macros

Syntactic Extension with Elixir Macros

An Elixir program can be represented by its own data structures as an abstract syntax tree (AST) of tuples with three elements. For example, the function call run(1, 2, 3) is represented internally as:

{:run, [], [1, 2, 3]}

Macros in Elixir lets you perform syntactic extensions, which are expanded to before a program executes. We use macros to transform our internal program structure by treating code as data, and thus metaprogram.

Many of Elixir’s language features such as if and def are in fact macros, which are translated away into lower-level abstractions during compilation. Armed with the same tools that José utilized to write Elixir’s standard libraries, you can extend Elixir to suit your needs using macros.

defmodule MyModule do
  defmacro macro(code) do
    IO.inspect code
    code
  end
end
iex> require MyModule
iex> MyModule.macro((1+2)* 3)
{:*, [line: 3], [{:+, [line: 3], [1, 2]}, 3]}
iex> MyModule.macro(IO.puts("Sup"))
{ {:., [line: 4], [{:__aliases__, [counter: 0, line: 4], [:IO]}, :puts]},
 [line: 4], ["Sup"] }

In the snippet above, we defined a macro with defmacro, and take in a block of code as arguments. Macros in Elixir transforms the block of code into its internal Abstract Syntax Tree (AST) representation, which is a nested tree of triples (3-size tuples.) When the macro macro returns code, that internal representation is injected back into the global program’s compile tree. In the above example, we just return code unmodified.

What’s most useful here is that we can modify that internal representation before returning, transforming it into a completely different piece of code. We can have both performant and concise code by hiding away low-level optimizations within high-level macros.

defmodule MyModule do
  defmacro macro(code) do
    IO.inspect code
    newcode = quote do: IO.puts "Whatever."
    IO.inspect newcode
    newcode
  end
end
iex> MyModule.macro((1+2)* 3)
{:*, [line: 10], [{:+, [line: 10], [1, 2]}, 3]}
{ {:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["Whatever."]}
Whatever.
:ok

Note that code was never executed/evaluated, only the returned newcode was executed.

quote lets us transform a block of code into its internal AST representation. Internally, defmacro calls this method to passed in parameters.

In the example above, we return a new internal representation newcode instead of the original code. Running the macro on any block of code results in the insertion and execution of our newcode.

What if we want to evaluate a code block? unquote lets you defer execution of the code block it receives, only running it when the code generated by quote is executed. You can only use unquote inside quote blocks.

Here’s an example:

defmodule MyModule do
  defmacro macro(code) do
    IO.inspect code
    quote do
      result = unquote(code)
      IO.inspect result
      case result do
        val when is_number(result) -> result
        _ -> IO.puts "Not a number"
      end
    end
  end
end
iex> MyModule.macro(1+2*3)
{:+, [line: 40], [1, {:*, [line: 40], [2, 3]}]}
7
7
iex> MyModule.macro("Hello" <> " world")
{:<>, [line: 42], ["Hello", " world"]}
"Hello world"
Not a number
:ok

In the above example, we evaluate code using unquote and use its result to perform some computation dispatched via pattern matching.

You can find more examples of macros in Phoenix and ExUnit.

In Closing

Macros lets you grow from a language consumer to a language creator. Elixir’s Open Language takes a step forward from Open Classes in Ruby. Armed with the same tools that José utilized to write Elixir’s standard libraries, you can extend Elixir to suit your needs, with features that are as first-class as if and def.

Additional reading:

Author

Yos is a software craftsman based in Singapore.

📬 Subscribe to my newsletter

Get notified of my latest articles by providing your email below.


Going Serverless book

Interested to find out more about serverless? Going Serverless teaches you how to build scalable applications with the Serverless framework and AWS Lambda. You'll learn how to design, develop, test, deploy, and secure Serverless applications from planning to production.

Learn More →