The code example on page 22 is intended to show how to re-create Elixir’s if
macro. However, it uses if
on line 3.
The macro block just passes the args to if
. This could be a typo and meant to be my_if
. If that’s the case, a simple substitution would result in the error ... cannot invoke macro my_if/2 before its definition
.
defmacro my_if(expr, do: if_block), do: if(expr, do: if_block, else: nil)
^^
That leads me to believe the example can be re-written to be more similar to Elixir’s own implementation^1, e.g.
defmodule ControlFlow do
defmacro my_if(expr, clauses) do
build_my_if(expr, clauses)
end
defp build_my_if(expr, do: if_block) do
build_my_if(expr, do: if_block, else: nil)
end
defp build_my_if(expr, do: if_block, else: else_block) do
quote do
case unquote(expr) do
result when result in [false, nil] -> unquote(else_block)
_ -> unquote(if_block)
end
end
end
end
Same idea, but instead of 2 macros we have 1 macro that passes to a private function with 2 clauses.
Further, using the built-in if
leads to an unexpected expansion, using the example:
iex> quote do ControlFlow.my_if 1 == 1, do: :ok end |> Macro.expand_once(__ENV__)
:ok
I believe this is because Elixir’s implementation further reduces tautologies^2, which the example doesn’t. From the example, we’d expect to see :case
in the resulting AST (this confusion is what lead me here originally).