Self-Documenting Code is Real with Elixir Macros

One of the things I love about the Elixir language is that documentation is a first-class feature. And rather than code comments, which are removed at compile-time, it uses attributes for documentation, which are not. That has some very nice implications when you’re writing macros that define functions.

Before we begin, I should note that the Elixir guides themselves say to use macros responsibly. They are very powerful, and they can make code difficult to read and interpret.

So one way I like to use macros is to improve the compile-time guarantees of functions that have a limited set of valid arguments. The following say/1 function below has runtime checks in the form of pattern-matching, but it does not have a strong compile-time check for valid arguments:

defmodule MyApp.Sayings do
  @doc "Says "Hello""
  def say(:hello), do: "Hello"
  @doc "Says "World""
  def say(:world), do: "World"
end

So, if you write your function call:

MyApp.Sayings.say(:something_else)

…The compiler will not complain. But of course, when your app actually calls the function, it will fail because it doesn’t match any of the function signature patterns.

Imagine that instead of just returning strings, you were checking a whitelist to see if a user had a named permission. If you mistyped the permission name, you might just lock out every user. It would be nice to have a way to eliminate the possibility of this error at compile-time.

What if we wrote a module that looked more like this?

defmodule MyApp.Sayings do
  def say_hello(), do: "Hello"
  def say_world(), do: "World"
end

Now, if you call:

MyApp.Sayings.say_something_else()

…That function does not exist. The compiler will yell at you. Nice. But what if there are 40 different sayings? That would make this code tedious to write and maintain. That’s where a macro comes in. Let’s give ourselves a less repetitive way to define these types of functions…

defmodule MyApp.Sayings.Macro do
  @doc """
  When applied like `defsay(:foo, "bar")` it defines a function
  like `def say_foo(), do: "bar"`.
  """
  defmacro defsay(name, value) when is_bitstring(value) do
    quote do
      @doc """
      Generated via the `defsay/2` macro.</code>
      Says \"#{unquote(value)}\".
      """
      def unquote(:"say_#{name}")(), do: unquote(value)
    end
  end
end

defmodule MyApp.Sayings do
  import MyApp.Sayings.Macro
  # Now we use the macro to define our functions.
  defsay(:hello, "Hello")
  defsay(:world, "World")
end

That last module definition is equivalent to:

defmodule MyApp.Sayings do
  import MyApp.Sayings.Macro
  @doc """
  Generated via the `defsay/2` macro.
  Says "Hello".
  """
  def say_hello(), do: "Hello"
  @doc """
  Generated via the `defsay/2` macro.
  Says "World".
  """
  def say_world(), do: "World"
end

Notice that our macro generated @doc attributes along with the functions. Our documentation is written in attributes, not comments, so it doesn’t get removed, and it will show up when you generate your Hex docs HTML.

Voila! Self-documenting code. Sort of.

Revelry leads digital innovation that has a real impact on business goals.

Each member of our team is pivotal to our process.
Get to know us by checking out our About Page!
We build open source projects and we’re looking for contributors like you.
Have a look: Revelry on Github or Come work with us!