Advice for working around my _only_ problem with "with" expressions - accessing success results

Consider this code:

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:

  1. What is the best way to achieve the desired outcome? Should I just use two separate case expressions?
  2. 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? :thinking: 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.

P.S. I have already read https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1. It just says “the chain is aborted”.

P.P.S. get your SO points here: https://stackoverflow.com/questions/63394081/advice-for-working-around-my-only-problem-with-with-expressions-accessing

1 Like

This works:

    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"}
4 Likes

Perfect!

Are you going to answer on SO or will I answer it myself?

Why is this not in the docs? :frowning:

Go for it :+1:

1 Like

This forum is so darn good. If it had dark mode it would be the best website on the internet.

4 Likes

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.

1 Like

+1 for Ecto.Multi. Seems much nicer for this use-case.

Lately I use this approach for most of my code and it works well for me: https://luizdamim.com/blog/reorganizing-your-phoenix-contexts-as-use-cases/

2 Likes

How would you use Ecto.Multi for this case?

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.

I have read that blog post. It’s a good tip for code organization.

For complex multi-step processes where rollback steps are necessary, you might want to look into the “saga” pattern.

There is Sage which is an implementation of the saga pattern in Elixir.

5 Likes

That is a very good tool! I will use Sage for anything where I have to make more than 1 external API call.

Thanks for sharing the good information.

1 Like

I just want to say thanks again for showing Sage to me. I have been using it for a few months and it makes my life so much easier.

4 Likes