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
:
-
:"Elixir.Kernel".in
makes a call toin/2
which is actually just a macro forEnum.member?/2
-
Enum.member?/2
checks whether an element is within an enumerable - So,
:"Elixir.Kernel".in(x, [false, nil])
checks whetherx
(the condition) is equal to eitherfalse
ornil
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.
More Content
If you liked this, you might also like some of my other posts. If you want to be notified of my new posts, follow me on Dev or subscribe to my brief monthly newsletter.
- What's the best questions you've been asked in a job interview?
- What was the first program you wrote?
- The weird quirk of JavaScript arrays (that you should never use)
- Does Elixir have for loops?
- Learn Elixir with me!
- Project Tours: Bread Ratio Calculator
- Changing Emoji Skin Tones Programmatically
- I made my first svg animation!
- 5 tips for publishing your first npm package
- 4 Hugo Beginner Mistakes
Top comments (0)