April 5, 2018

Don't Try This at Home: Building a Return Statement in Elixir

tldr; now you can feel like you're writing in Ruby again

Don't Try This at Home: Building a Return Statement in Elixir
defmodule YourModule do
  use Returnable
  
  defr sample_fun(x) do
    if x == 5 do
      return :awesome
    end
    
    x + 5
  end
end

iex> YourModule.sample_fun(5)
:awesome
iex> YourModule.sample_fun(7)
12

With the help of three simple macros you start breaking all the rules. Makes you feel pretty cool yeah?

This is available as a package on Github. Maybe I'll publish to hex? Special thanks to Steven Proctor and the rest of the DFWBeamer meetup for helping with the idea. A lot of inspiration from this post too.

return, the Return Statement that Throws

This is the simplest of the macros. return expr throws whatever value expr evaluates to. This makes it easy to break out early.

defmacro return(expr) do
  quote do
    throw({unquote(__MODULE__), :return, unquote(expr)})
  end
end

ret, the Returnable Block

Whereas return throws, a ret block catches, immediately returning that value.

# Strip off the nested do's
defmacro ret(do: expr) do
  quote do
    Returnable.ret(unquote(expr))
  end
end

defmacro ret(expr) do
  quote do
    try do
      unquote(expr)
    catch
      :throw, {unquote(__MODULE__), :return, val} -> val
    end
  end
end

This allows you write things like:

ret do
  x = val + 5
  if x == 10, do: return :invalid
  x + y
end

x + y will only be evaluated and returned from this block if x is not 10, else :invalid will be returned.

defr, the Returnable Function

With a return statement and returnable block in place, writing a returnable function as easy as creating a macro to wrap the def function body in a ret block.

defmacro defr(call, expr) do
  quote do
    def unquote(call) do
      ret do
        unquote(expr)
      end
    end
  end
end

Final Thoughts

While I don't recommend ever using this package seriously, it does change the way we look at Elixir functions. For better or worse, you can now return from list comprehensions early.

defr fetch_user(users, id) do
  for user <- users do
    if user.id == id, do: return user
  end
end

Have suggestions? Raise an issue on the Github repo. Contributions are welcome!