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
func1andfunc2are writing to the db. Iffunc1returns an error I don't wantfunc2to 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