Hi there !
This week at CodeBEAM I had a chance to meet an amazing crowd and participate in some interesting conversations. One of them was with more experienced with erlang folks discussing new maybe_expr
in OTP-25. @max-au raised concerns about it which I initially didnāt get right.
Iāve been happily writing elixir and using with
for years by now, thinking thatās the right way to handle cleanly the scenario of nested branching expressions with a single happy path. And for some reason I never questioned why throw/catch
is reserved as some sort of a ālast resortā for libs that donāt return :ok/:error tuples but throw
in error case.[*]
Also as mentioned in EEP-0049#Elixir
Elixir has a slightly different semantic approach to error handling compared to Erlang. Exceptions are discouraged for control flow (while Erlang specifically uses
throw
for it), and thewith
macro is introduced
Example:
Here, is the synthetic example that hopefully will help to show the idea:
Say I want to put together a function that does multiple things with given argument:
defmodule Foo do
def bar(num) do
num
|> double()
|> subtract(num)
|> divide(num)
|> multiply(num)
end
defp double(num), do: 2 * num
defp subtract(a, b), do: a - b
defp divide(a, b), do: a / b
defp multiply(a, b), do: a * b
end
Often in real world some of these private functions are expected to return erroneous results⦠often we wrap the result in :ok and :error tuples and use with
special form.
Here is the example of the same module, but where each private function might return either :ok or :error tuple and parent function uses with
to focus on happy path and still be aware of possible erroneous results:
defmodule Foo do
@spec bar(number) :: {:ok, number} | {:error, :not_number | :zero_division}
def bar(num) do
with {:ok, double_num} <- double(num),
{:ok, subtracted_num} <- subtract(double_num, num),
{:ok, divided_num} <- divide(subtracted_num, num) do
multiply(divided_num, num)
end
end
defp double(x) do
if is_number(x),
do: {:ok, 2 * x},
else: {:error, :not_number}
end
defp subtract(a, b) do
if is_number(a) and is_number(b),
do: {:ok, a - b},
else: {:error, :not_number}
end
defp divide(_a, 0), do: {:error, :zero_division}
defp divide(a, b) do
if is_number(a) and is_number(b),
do: {:ok, a / b},
else: {:error, :not_number}
end
defp multiply(a, b) do
if is_number(a) and is_number(b),
do: {:ok, a * b},
else: {:error, :not_number}
end
end
However, we could have achieved the same behavior without need to wrap everything into āmonadicā :ok/:error tuples, just throw
when something goes wrong and let parent catch
it. As a bonus we can again use pipes (though, thatās not the priority point at this moment).
defmodule Foo do
@spec bar(number) :: number | :not_number | :zero_division
def bar(num) do
num
|> double()
|> subtract(num)
|> divide(num)
|> multiply(num)
catch
error -> error
end
defp double(x) do
if is_number(x),
do: 2 * x,
else: throw(:not_number)
end
defp subtract(a, b) do
if is_number(a) and is_number(b),
do: a - b,
else: throw(:not_number)
end
defp divide(_a, 0), do: throw(:zero_division)
defp divide(a, b) do
if is_number(a) and is_number(b),
do: a / b,
else: throw(:not_number)
end
defp multiply(a, b) do
if is_number(a) and is_number(b),
do: a * b,
else: throw(:not_number)
end
end
Apparently, this style is used in Erlang quite often.
Now Iām really puzzled, why it is discouraged in elixir?
[*] elixir-lang.org | Getting Started / Try, catch and rescue / Throws
Those situations are quite uncommon in practice except when interfacing with libraries that do not provide a proper API