with {:a, first_result} <- {:a, List.first(["apple"])},
{:b, b} <- {:b, %{} |> to_string} do
{:ok, "happy path"}
else
{:a, _} -> {:error, "this will never be a problem"}
{:b, error} -> {:error, "Why can I not print #{first_result} here?"}
end
I have learned by trial-and-error that this code will not return
{:error, "Why can I not print apple here?"}
as I originally expected from this kind of syntax. Instead, the compiler tells me that first_result is not defined in the else block. It makes sense, because the “clause chain” returns the error result of the failing clause.
So I have 2 questions:
What is the best way to achieve the desired outcome? Should I just use two separate case expressions?
Imagine if the first clause adds something to the database with Repo.insert. Does Ecto know it is inside a with expression and wait for all clauses to succeed before committing the transaction? That seems unlikely. So then, why does this expression not allow us to access any successful results?
It seems like I should be very careful when using with since, if I understand correctly (I probably don’t), I can have “partial success” without being able to see the successful results.
Any advice and insight is welcome! I hope the question is clear.
with {:a, first_result} <- {:a, List.first(["apple"])},
{:b, first_result, :something} <- {:b, first_result, :other_thing} do
{:ok, "happy path"}
else
{:a, _} -> {:error, "this will never be a problem"}
{:b, first_result, _} -> {:error, "Now I can print #{first_result} here"}
end
# => {:error, "Now I can print apple here"}
This is kind of a philosophy thing so it can’t be repeated everywhere. You have to learn to think in Erlang’s structures, tuple being one of them. Since the BEAM doesn’t have static typing, people have resorted to various workarounds to concretely match on intermittent values during their workflow, with tuples being the main way of doing it. You can devise your own structures with them and pass them around – as long as the consuming code is aware of them everything will be pretty solid.
This is kind of how the {:ok, value} and {:error, message_or_exception} came to be. This idiom is merely a convention, nobody mandates you to use it in your own code.
Another way is how Ecto.Multi does it: you can just pattern-match on a map of successful previously executed steps as you go.
Clause 1 adds an order to the DB, with validation done on the changeset
Clause 2 pays for the order using a 3rd party API.
If payment fails, the order has been created and I want to update its state/status to payment_failed.