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.