Practical Elixir Macro Examples
While I haven't written about it here yet, much of my past year has been spent learning and writing Elixir.
As I have started to delve beyond surface-level language features I have found topics that are not discussed to my satisfaction (or at least in words that I think make sense) so I am determined to document my adventures here.
One such feature I recently started using that fit this bill is the macro. A macro in Elixir is a feature that essentially enables a sort of metaprogramming. For example, say you have several modules that all do the same things essentially, but in a different context - like notifications.
You might have MyApp.SMS
, MyApp.Email
and so on, where each module has common functions that they need. One option would be to write each module with all the functions it needs. Another would be to extract it to another module. But what if you could utilize the design approach of separating things into modules, without extracting and also without repeating yourself? Enter macros!
Here are some example code snippets without macros:
defmodule MyApp.SMS do
def send(target, body) do
# Your logic
end
def create_body(body) do
# Shared function
end
end
defmodule MyApp.Email do
def send(target, body) do
# Your logic
end
def create_body(body) do
# Shared function
end
end
In this example imagine that create_body/1
does something that both modules will need. Perhaps it will take a body string and replace certain characters, sanitize it, or something along those lines. You could write it each time like I did above, or maybe even move it to a common module, but with macros you could bake in this behavior using using
.
defmodule MyApp.NotificationFramework do
def __using__(_opts) do
defmacro do
def create_body(body) do
# Shared function
end
end
end
end
With this macro created, we can now use
it in one of the modules above:
defmdoule MyApp.Email do
use MyApp.NotificationFramework
def send(target, body) do
# Your logic
end
end
use
differs from import
or require
in that it essentially allows the module you are use
ing to arbitrarily insert things into the context that use
d it. In this case, that means that it is inserting the macroed create_body/1
function. At run time, the module as written above is essentially the same as the one with the use
clause.
Here is what the module really looks like after the use
:
defmodule MyApp.Email do
def send(target, body) do
# Your logic
end
def create_body(body) do
# Shared function
end
end
This can essentially create modules that are similar with the exception of maybe one or two functions that must be implemented differently, but should be self contained for design reasons (like ease of reading, or something similar).
Note there are some caveats around when the things in the macro are inserted into the context where use
is called - in general the macro is compiled late so it will not overwrite things defined in the module directly. There are also some considerations around declaring variables - I recommend referencing the official documentation if this is an area you want to know more about.
There are other uses for macros that I have not yet explored, but I will certainly write more about them as I learn more.