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.