Proper way of handling simple booleans in `with` statement

Just to throw some grenades, Jose on tagged with on the ThinkingElixir Podcast and relatedly keathleys Good Bad Elixir.

See also the beware section in the with docs, and a similar question I directed at Saša Jurić.

Jose’s argument (and basically every other “anti-tagger” I have read/heard) is that with is explicitly made to handle the case [sic] where your failure states are pretty unified. “If the error matters, use case” sticks in my head.

My take away is that the else is actually almost an after thought for some edge cases, and that most uses of with probably shouldn’t need/have one.

Do I agree with this? Not really sure. I dont really like nested case statements:

case is_integer(foo) do
  true ->
    case rem(foo,2) == 0 do
      true ->
          case 
          ...  # blergh :vomit:

You could wrap each step in its own ensure_integer, ensure_rem2 function, etc that return :ok | {:error, :x} but that feels like a lot of work for some simple checks.

You could write

def validate_foo(foo) when is_integer(foo) and rem(foo,2) == 0 and foo > 50 do
:ok # or act_foo(foo), whatever
end

def valiate_foo(foo) when is_integer(foo) and rem(foo, 2) == 0 do
  {:error, :under_50}
end


def valiate_foo(foo) when is_integer(foo) do
  {:error, :not_rem_2}
end

but that’s not great either IMO because the logic becomes pretty complicated to keep track of.

I think some of the problem is we reduce these problems down to talk about them when really that abstracts any ability to reason on why we should or shouldn’t use a form. In this case does it matter if your foo is bad? can you recover? is passing {:error, reason} useful?

It’s trite, but “know the rules to break the rules” is a valid idiom.

In some cases, tagging the tuple is just the most ergonomic way to do it. In other cases you may want to wrap it in a data structure (such as using Ecto changesets, which are great outside of Ecto) or use a more structured set of functions or case statements.

I would posit that if you want to return a reasonable error back to the user (i.e. they provided a foo of bad quality), then building a changeset or similarly robust structure around it isn’t a bad idea, because you’re now talking about a business rule which can probably stand to be codified more concretely.

Otherwise perhaps you really only need to return {:error, :invalid_foo} and the UI should just be explicitly stating the qualifiers around foo (must be even, must be over 50, etc).

I do like both eksperimentals x || :error and kartheeks error_* styles.

14 Likes