1. 10
  1. 12

    Another solution, not mentioned here, which we used in my last place: one-off tags for the right side of the with clauses to help differentiate. It worked pretty well.

    with {:get_user, {:ok, user}} <- {:get_user, get_user()},
         {:get_permissions, {:ok, permissions}} <- {:get_permissions, get_permissions(user)} do
       ...
    else
       {:get_user, :err} -> ...
       {:get_permissions, :err} -> ...
    end
    
    1. 1

      FWIW, I have always preferred nested case to this style. But it does work and it isn’t ambiguous.

      1. 1

        I ended up doing something similar in my most recent Elixir/LiveView project, except that instead of using tag-tuples, I used functions that’d return known invalid atoms.

        https://github.com/yumaikas/dream_crush_score/blob/master/lib/dream_crush_score/room/room.ex#L375-L390

      2. 3

        I tend to agree. I never use with mostly because it just feels too fancy to me. I can’t easily reason when it is doing. It might just be me not trying to. I always had the same problem in perl with unless, until, and when.

        1. 2

          I use them a lot when otherwise I would use case with one match, ex.:

          with {:ok, record} <- Repo.insert(changes) do
            Repo.preload(record, [:foos])
          end
          

          Or when I care only about success/error, and I want the other value to be passthrough. For example imagine that you have set of caches, then you can do:

          with :error <- cache_1(), # All of these return {:ok, value} on success and :error on miss
               :error <- cache_2(),
               :error <- cache_3(),
               do: :error
          
          1. 1

            ah, that is helpful. Thank you.

        2. 2

          I’m still confused. Isn’t the whole point of with that you want to collapse all the error handling into a single block? Why would you use it if you didn’t want this behavior?

          1. 7

            Yeah the way I’m thinking of it, the problem here is either:

            • the error handling should be general and not necessarily say which call failed or why
            • the error handling should be specific and therefore the with construct’s else block is inadequate

            The article chose to weight on the latter but the argument for the first option is also a possibly valid one if you make it part of your validation contract to return an error reason such as {:error, step, reason} and return these as if they were part of a contract.

            Or alternatively and in the examples of the blog post, have parse_schema(...) return {:error, :schema_unparseable} as a value (and provide a wrapper if it is a library function) so that it can be generically handled.

            But either way, clarity requires a better delineation of where responsibility sits for the translation of more opaque internal validation errors into user-readable content vs. the control flow that is chosen.

            1. 1

              That’s a good way to put it, yeah.

              This is interesting to me because we recently introduced a match-try macro to Fennel with similar semantics to with (but a better name): https://fennel-lang.org/reference#match-try-for-matching-multiple-steps

              I’ve found it extremely useful for situations where you specifically want any failure that might happen in a series of steps all to be handled the same way; maybe they should always print an error message, or maybe they should always just short-circuit and return nil.

              Obviously such a construct can be abused; maybe this is a situation where the problem is that people misunderstand what it’s for, not a problem with the construct itself. (Or maybe the problem is not enough monads; who knows.)

          2. 2

            Seems as though Erlang/OTP will be adopting a similar construct quite soon: https://www.erlang.org/eeps/eep-0049

            I think I’d have to play with this to get a feel for it – not just read source code – but I have a slight itchiness forming that Let It Crash (an Erlang koan) and these patterns could be at odds. Not to say that all langs should follow Let It Crash, but in the case of Erlang, it’s so typical to skip writing defensive lines of code.

            1. 2

              I’m not familiar with Elixir, and I can’t figure out the semantics of with even after a few minutes of effort..

              1. 6

                It’s repeated matches that fall through to the else clayse on the first mismatch, giving the mismatched value, which can then also be pattern matched against.