Written December 3, 2015. Tagged Elixir, Macros.
Exploring Elixir, I tried this:
defmodule Example do
def foobar(do: _, else: _) do
end
end
Example.foobar do
IO.puts "true"
else
IO.puts "false"
end
And it worked. Well, in a sense. The code runs, but it outputs both "true" and "false".
What's going on here? Let's try another experiment:
IO.inspect do
"true"
else
"false"
end
# => [do: "true", else: "false"]
Turns out this is language-level syntactic sugar (e.g. 1, 2, 3) that desugars to a plain keyword list. And that explains why the code above would output both "true" and "false" – it's equivalent to
Example.foobar([do: IO.puts("true"), else: IO.puts("false")])
The keyword list is evaluated before it's even passed to the function, like any keyword list would be. That includes evaluating the IO.puts
function calls.
Now that we have the full list of block keywords (from the Elixir source) we can go completely crazy:
IO.inspect do
"a"
else
"b"
catch
"c"
rescue
"d"
after
"e"
end
# => [do: "a", else: "b", catch: "c", rescue: "d", after: "e"]
So these are all available to your own functions, like in our Example.foobar
example. But what use are they if every branch is evaluated all the time?
Macros to the rescue.
Elixir macros get access to the syntax tree of a piece of code, without the code being evaluated first. They can then slice and dice the code and return another syntax tree, that will be evaluated.
Elixir's own if/do/else
is just a macro using these keyword lists.
Just for fun, we could make a macro that randomly executes one of two branches, and then always runs the after
branch:
defmodule MyMacro do
defmacro pick([do: option1, else: option2, after: after_block]) do
:random.seed(:os.timestamp)
option = Enum.random([option1, option2])
[option, after_block]
end
end
defmodule Example do
require MyMacro
def run do
MyMacro.pick do
IO.write "dog"
else
IO.write "cat"
after
IO.puts "!"
end
end
end
Example.run
# Outputs either of these:
# dog!
# cat!
I haven't used this myself, other than in silly experiments. I can picture it being handy for some DSLs, though. If you apply this to anything interesting, please do let me know in a comment!