Elixir block keywords

Written . 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!