Reading the Elixir source code to learn how if blocks work
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"
endLet’s break this down…
Defining the macro
defmacro if(condition, clauses) do
build_if(condition, clauses)
endIt 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"
endThis 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
)
endNote: 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:
:"Elixir.Kernel".inmakes a call toin/2which is actually just a macro forEnum.member?/2Enum.member?/2checks whether an element is within an enumerable- So,
:"Elixir.Kernel".in(x, [false, nil])checks whetherx(the condition) is equal to eitherfalseornil
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.
If you liked this post, click here to subscribe to my mailing list!