William Schroeder
William Schroeder
Jun 6, 2023 2 min read

Simplify Function Generation in Elixir with Macros

In Elixir, generating functions can be a powerful technique that goes beyond its applications in Matrix Testing. One such scenario is when you want to eliminate boilerplate code while creating multiple factory functions that share strong similarities within your ExMachina modules. In this blog post, we’ll explore how macros can simplify the process of function generation, leading to cleaner and more maintainable code.

Let’s start with an example of the functions we want to abstract:

defmodule Fruit do
  @doc "Returns apple"
  def my_apple do
    "apple"
  end

  @doc "Returns orange"
  def my_orange do
    "orange"
  end

  @doc "Returns pear"
  def my_pear do
    "pear"
  end
end

Similar to what we did in Matrix Testing and in spirit to how we refactored an existing codebase with DRY principles, we can generate these functions with a for comprehension:

defmodule Fruity do
  @fruit ~w[apple orange pear]

  for f <- @fruit do
    @doc "Returns #{f}"
    def unquote(:"my_#{f}")() do
      unquote(f)
    end
  end
end

IO.puts(Fruity.my_apple())

In the above code, we define the Fruity module and use a for comprehension to generate multiple functions based on the @fruit list. Each generated function has a similar structure but with a different name and return value. Note how we use unquote outside of an obvious defmacro (hint: what is def?) in order to create the function name.

Suppose we wanted to use the Fruity module and import these functions as part of the module’s code generation. Here’s an example of the same function generation within a defmacro block:

defmodule Fruity do
  @fruit ~w[apple orange pear]

  defmacro __using__(_) do
    for f <- @fruit do
      quote do
        @doc "Returns #{unquote(f)}"
        def unquote(:"my_#{f}")() do
          unquote(f)
        end
      end
    end
  end
end

defmodule Example do
  use Fruity

  def print_fruity do
    IO.puts my_apple()
  end
end

Example.print_fruity()

In this second version, we define the Fruity module with a defmacro block instead of regular functions. The __using__ macro is invoked when the module is used in another module (i.e. use Fruity), allowing us to generate the desired functions at compile time. The generated functions are identical to those in the previous example.

In Elixir, macros serve as the highest level of abstraction, enabling developers to shape the language itself to suit their needs. Function generation through macros is a prime example of harnessing this power, allowing for the elimination of repetitive code and the creation of clean, concise, and maintainable codebases.