Reading the Elixir source code to learn how if blocks work

by Adam Davis

Table of contents:

As I’ve been working with Elixir, I’ve been wondering about the ways that macros can be leveraged. So once I realized that if statements are implemented as macros, I figured that reading the source code would be a good way to learn from the creators of the language.

Finding the code

In the Elixir documentation, every function has a </> button that links to its source code. If you follow that button in the documentation for if/2, you’ll find the code that makes if statements work. I’ve pasted it below:

defmacro if(condition, clauses) do
  build_if(condition, clauses)
end

defp build_if(condition, do: do_clause) do
  build_if(condition, do: do_clause, else: nil)
end

defp build_if(condition, do: do_clause, else: else_clause) do
  optimize_boolean(
    quote do
      case unquote(condition) do
        x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
        _ -> unquote(do_clause)
      end
    end
  )
end

defp build_if(_condition, _arguments) do
  raise ArgumentError,
        "invalid or duplicate keys for if, only \"do\" and an optional \"else\" are permitted"
end

Let’s break this down…

Defining the macro

defmacro if(condition, clauses) do
  build_if(condition, clauses)
end

It starts out by defining a macro for if/2 that accepts the parameters condition and clauses. While the documentation says “this macro expects the first argument to be a condition and the second argument to be a keyword list” there is no programmatic enforcement of that at this level.

Within the macro’s body, all we have is a call to build_if/2 that passes along the unmodified parameters of if/2.

build_if/2

defp build_if(condition, do: do_clause) do
  build_if(condition, do: do_clause, else: nil)
end

defp build_if(_condition, _arguments) do
  raise ArgumentError,
        "invalid or duplicate keys for if, only \"do\" and an optional \"else\" are permitted"
end

This is a private function called by the macro and exists in two forms.

The first instance of this function has pattern matching for the clauses argument to only accept a value that starts with do. If the pattern matching succeeds, a call is made to build_if/3 with a value of nil for the else clause.

The second instance does not use pattern matching, but instead has an underscore at the beginning of the parameter names to indicate that their values will not be used. This function raises an ArgumentError to show that the requisite pattern matching for the do clause has not been met.

build_if/3

defp build_if(condition, do: do_clause, else: else_clause) do
  optimize_boolean(
    quote do
      case unquote(condition) do
        x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
        _ -> unquote(do_clause)
      end
    end
  )
end

Note: I’m not quite sure what the purpose of optimize_boolean is, but the source code for it is here if you want to take a look at it. If you know what it’s purpose is, let me know!

This function is where the actual logic of the if is carried out.

It makes use of quote/2 and unquote/1. Quoting returns the internal representation of an expression, and unquoting gets an expression from an internal representation. You can read more about them here.

First, we enter a call to quote/2 and start a case statement that unquotes the condition parameter in order to evaluate it.

Let’s break down the first pattern in the case:

  1. :"Elixir.Kernel".in makes a call to in/2 which is actually just a macro for Enum.member?/2
  2. Enum.member?/2 checks whether an element is within an enumerable
  3. So, :"Elixir.Kernel".in(x, [false, nil]) checks whether x (the condition) is equal to either false or nil

From there, we see that condition evaluating to either false or nil will result in else_clause being returned.

The default case returns the do_clause, which is what we would expect to be returned if the condition was truthy.

Conclusions

We can draw a few interesting observations from reading through this code. The first is that truthiness in Elixir’s if statements is defined as being neither false nor nil. This means that if/2 can be a simple way to test whether a variable has neither of those values.

The other lesson is that macros can work well for implementing control flow. By passing the do and else clauses around as keyword lists, it prevents that code from being run unless we decide it should run.

Adam Davis

Share this post: