Let's say I have a function main_function
that depends on the result of three other functions which can each either return {:ok, result}
or {:error, error}
. How can I avoid having deeply nested case statements that feel like callback hell in javascript.
Example:
def func1(input) do
case SOMECONDITION do
{:ok, result} ->
{:ok, result}
{:error, error} ->
{:error, error}
end
end
def func2(input) do
case SOMECONDITION do
{:ok, result} ->
{:ok, result}
{:error, error} ->
{:error, error}
end
end
def func3(input) do
case SOMECONDITION do
{:ok, result} ->
{:ok, result}
{:error, error} ->
{:error, error}
end
end
def main_function(input) do
case func1(input) do
{:ok, result} ->
case func2(result) do
{:ok, result} ->
case func3(result) do
{:ok, result} ->
{:ok, EXPECTED_OUTCOME}
{:error, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}
end
{:error, error} ->
{:error, error}
end
end
end
This just doesn't feel right...
[Edit]: I've added a convenience link to a great talk that covers this concept, as well as solutions for more sophisticated needs from ElixirConf 2018 below.
Don't worry - Elixir has you covered. You want this special form: with/1
with/1
will continue executing functions if and only if they match the expected outcome.
You're main function essentially looks something like:
def main_function(input) do
with {:ok, result_1} <- func1(input),
{:ok, result_2} <- func2(result_1),
...,
do: {:ok, EXPECTED_OUTCOME}
end
When it can't find a match, say because there's a tuple like {:error, _error}
the special form will return the first error encountered and stop executing the functions.
You can also add an else condition. An example where I've used this is when a user may expect some action that requires a lot of functions to complete, and I want to alert them of the same thing regardless of where it failed:
def main_function(input) do
with {:ok, result_1} <- func1(input),
{:ok, result_2} <- func2(result_1),
... do
{:ok, EXPECTED_OUTCOME}
else
_error ->
{:error, "Couldn't complete action"}
end
end
Here's an amazing talk from the author of Credo on this concept, courtesy of ElixirConf 2018: https://www.youtube.com/watch?v=ycpNi701aCs&t=473s
Here's a great post on with/1
: https://www.erlang-solutions.com/blog/exploring-with-the-elixir-special-form.html
Answering the comment on the accepted answer:
That is an interesting approach. But let's say
func1
andfunc2
are writing to the db. Iffunc1
returns an error I don't wantfunc2
to be executed at all. Would this be the case?
There is also another option, if you are using Ecto.
Let's say func1
, func2
, and func3
all write to the DB, but the steps fail at func2
, which means func1
has already written to the DB.
And let's say they return {:ok, result}
or {:error, error}
.
Ecto.Multi
to the rescue!
alias Ecto.Multi
alias MyApp.Repo
def main_function(input) do
result =
Multi.new()
|> Multi.run(:result1, fn _ -> func1(input) end)
|> Multi.run(:result2, &func2(&1.result1))
|> Multi.run(:result3, &func3(&1.result2))
|> Repo.transaction()
case result do
{:ok, %{result1: _, result2: _, result3: _}} ->
{:ok, EXPECTED_OUTCOME}
{:error, error, _changes_so_far} ->
{:error, error}
end
end
With Multi, returning an error tuple will abort any further operations and make the whole multi fail. Also, since it's using a transaction, any that succeeded will all be rolled back.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With