Pitfalls of Metaprogramming in Elixir
Tags: elixir
Published: 2021-11-23 16:05:48

Welcome back to this final part of our four-part series on [metaprogramming in Elixir](https://blog.appsignal.com/category/under-the-hood-of-metaprogramming.html).

Previously, we explored the various applications of macros.

In this part, we'll delve into common pitfalls that you might encounter when metaprogramming in Elixir.

## Common Perils of Macros
According to the [official documentation](https://elixir-lang.org/getting-started/meta/macros.html#write-macros-responsibly):

> Macros should only be used as a last resort. Remember that explicit is better than implicit. Clear code is better than
> concise code.

While it may be tempting to use metaprogramming for everything, it may not always be the best option.

The [Applications of Macros](https://blog.appsignal.com//2021/10/26/applications-of-macros-in-elixir.html) part of this series outlines the majority of use cases of macros.

However, you should only use macros with great caution.

We'll be looking at these three common pitfalls to avoid when using macros:

1. Injecting unnecessary functions
2. Over-injecting behavior
3. Replacing regular functions

Let's kick off by looking at what happens if you inject unnecessary functions into modules with macros.

**Note:** *These points are inspired by [Metaprogramming Elixir](https://pragprog.com/titles/cmelixir/metaprogramming-elixir/#resources).*

## 1. Injecting Unnecessary Functions with Macros

While macros can be used to inject functions into a caller, there are times where this is unnecessary.

Let's look at an example:

```elixir
defmodule CalculatorTransformer do
  defmacro __using__(_) do
    quote do
      def add(a, b), do: a + b
      def subtract(a, b), do: a - b
      def multiply(a, b), do: a * b
      def divide(a, b), do: a / b
    end
  end
end

defmodule Hospital do
  use CalculatorTransformer

  def calculate_cost(suite, procedure) do
    add(suite * 20, multiply(procedure, 5))
  end
end
```

We inject various calculator functions into `Hospital` to eliminate the need for a module identifier.

However, the use of `add` and `multiply` now seem like they appear from thin air.

The code loses its semantic meaning and becomes harder to understand for first-time readers.

To preserve the semantic meaning of the code, define `CalculatorTransformer` as a regular module. This module can be `import`ed into `Hospital` to eliminate module identifiers:

```elixir
defmodule CalculatorTransformer do
  def add(a, b), do: a + b
  def subtract(a, b), do: a - b
  def multiply(a, b), do: a * b
  def divide(a, b), do: a / b
end

defmodule Hospital do
  import CalculatorTransformer

  def calculate_cost(suite, procedure) do
    add(suite * 20, multiply(procedure, 5))
  end
end
```

As well as creating unnecessary functions, macros can also over-inject behavior into a module. Let's explore what this means.

## 2. Macros Over-injecting Behavior

As Elixir injects macros into the callsite, behavior can be over-injected.

Let's go back to the example of `BaseWrapper`:

What if we left the parsing logic for `post?` within the `__using__` macro?

```elixir
defmacro __using__(opts) do
  quote location: :keep, bind_quoted: [opts: opts] do
    # ...

    def post?(url, body) do
      case post(url, body) do
        {:ok, %HTTPoison.Response{status_code: code, body: body}} when code in 200..299 ->
          {:ok, body}

        {:ok, %HTTPoison.Response{body: body}} ->
          IO.inspect(body)
          error = body |> Map.get("error", body |> Map.get("errors", ""))
          {:error, error}

        {:error, %HTTPoison.Error{reason: reason}} ->
          IO.inspect("reason #{reason}")
          {:error, reason}
      end
    end
  end
end
```

There are two issues with this approach:

1. By testing `post?`, you test the inheritor rather than `BaseWrapper`.

    As there are multiple inheritors of `BaseWrapper` and the entire behavior of `post?` is injected into the inheritor, we have to test every inheritor individually.

    This ensures that any inheritor-specific behavior does not modify the behavior of `post?`.

    Failure to do so can lead to lower test coverage.
    

2. Ambiguous error reporting.

    Any run-time errors raised by `post?` will be logged under the inheritor, not `BaseWrapper`.

Therefore, leaving the entire behavior in `post?` can create confusion.

The original implementation of `BaseWrapper` moves the bulk of the parsing behavior into the wrapper instead. This implementation is much neater, semantically more meaningful, and readable.

This minimizes the two issues mentioned above, as:

1. When you test the core behavior of `post?`, only `BaseWrapper.parse_post` is tested — not every single inheritor.
2. Any errors from parsing will be logged under `BaseWrapper`.

    **Note:** `location: :keep` works in a similar fashion.

While we've used wrappers in our example of over-injecting behavior, this can equally apply to regular macros.

A rule of thumb is to minimize the amount of behavior in a macro.
Once the necessary information/computations that require a macro have been accessed/performed, you should move the remaining behavior out of the macro.

The final pitfall we'll examine is the use of macros when regular functions suffice.

## 3. Macros Used in Place of Regular Functions

As powerful as they are, you don't always need macros. In some cases, you can replace a macro's behavior with a regular function.

Let's say that behavior that does not require compile-time information (or a macro to perform computation) is placed in a macro, for example:

```elixir
defmodule Foo do
  defmacro double(x) do
    quote do
      doubled = unquote(x) * 2
      doubled
    end
  end
end

defmodule Baz do
  require Foo

  def execute do
    Foo.double(3)
  end
end

iex(1)> Baz.execute
6
```

Here, `double` could have easily been substituted for a regular function.  

Its behavior does not require compile-time information nor a macro for computation. It will be injected into `Baz` and evaluated when `execute` is called, just like a regular function.

```elixir
defmodule Foo do
  def double(x), do: x * 2
end

defmodule Baz do
  def execute, do: Foo.double(3)
end

iex(1)> Baz.execute
6
```

As you can see, defining `double` as a macro does not pose any benefits over a regular function.

## Metaprogramming in Elixir: Further Reading

We have finally come to the end of this investigation into metaprogramming in Elixir!

Remember: with great power comes great responsibility. Misusing metaprogramming can come back to bite you, so tread lightly.

While this series has aimed to explain metaprogramming and its intricacies concisely, it is by no means the "bible" on this topic.

There are many wonderful resources you can use to learn more about metaprogramming in Elixir! Here are just a few:

**Written guides**

  - [Understanding Elixir Macros - The Erlangelist](https://www.theerlangelist.com/article/macros_1)*
  - [Official Elixir tutorial on metaprogramming](https://elixir-lang.org/getting-started/meta/quote-and-unquote.html)
  - [Metaprogramming by Elixir School](https://elixirschool.com/en/lessons/advanced/metaprogramming/)
  - [Metaprogramming in Elixir - Serokell](https://serokell.io/blog/elixir-metaprogramming)

**Books**

  - [Metaprogramming Elixir](https://pragprog.com/titles/cmelixir/metaprogramming-elixir/)*

**Talks**

  - [ElixirConf 2017 - Don't Write Macros But Do Learn How They Work by Jesse Anderson](https://www.youtube.com/watch?v=Bo48sQDb-hk)

(* - *highly recommended*)

Thanks for reading, and see you next time!

**P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, [subscribe to our Elixir Alchemy newsletter and never miss a single post!](https://blog.appsignal.com/category/elixir-alchemy.html#elixir-alchemy)**

*Jia Hao Woo is a developer from the little red dot — Singapore! He loves to tinker with various technologies and has been using Elixir and Go for about a year. Follow his programming journey on [his blog](https://woojiahao.github.io/blog) and [Twitter](https://twitter.com/woojiahao_).*