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 useing to arbitrarily insert things into the context that used 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.