I recently wrote a function to do the following: look for a config variable, possibly under a namespace, in the given opts as well as the Application env. I’ve written this at least six times before, and none of them looked great if they could handle false values. Until I happened upon this lovely, somewhat cockeyed use of with:
def config(namespace \\ :none, key, opts) do
with nil <- opts[namespace][key],
nil <- Application.get_env(:my_app, namespace)[key],
nil <- @config_defaults[namespace][key],
nil <- opts[key],
nil <- Application.get_env(:my_app, key),
nil <- @config_defaults[key] do
nil
end
end
If I’m reading this correctly, it works by assuming that the clauses will return nil, and when one of them successfully does, it treats it as a matching error and implicitly returns it.
Normally you’d write something like:
with :ok <- do_thing() do
:success
else
{:err, :reason1} -> {:error, "reason 1 happened"}
{:err, :reason2} -> {:error, "reason 2 happened"}
_ -> {:error, "god only knows how we got here"}
end
It makes sense once you start thinking of with not in terms of success or failure, but in terms of match or no-match. If any of the with clauses return non-nil, that value is returned immediately. It is surprising at first, but it’s so much less ugly than nested cases!
Both case and with let you use guard clauses, which is handy.
It can be very helpful to use a tagged tuple in a with chain to make error handling easier: with {:step1, :ok} <- {:step1, do_thing()} can then let you match on {:step1, error} -> handle_step1_error(error) in the else branch.
You should probably always be using an else branch in your with statements to show you’ve considered all error conditions.
The |> is usually better handled via with due to error handling.
Lots of function heads instead of a single case statement can be faster but harder to maintain.
You’re far from the first person to advocate this style but I’ve never liked it in practice – too many else clauses feels like a smell, and having to crap up your with clauses with all those tuples starts to look worse than breaking each with clause into its own case and just having one function call another upon success. Logically, I like keeping the contents of each with clause paired with its error handling code more than I like stirring it all together.
No strong feelings here but if I don’t have a #3 in this list, Markdown calls the next line #3
Heh, I kid. I recently wrote a module that runs some periodic health checks inside one of our services (run queue depth is the most relevant one) and restarts the VM upon failure. I’m not just letting it crash, I’m making damn sure of it now!
But you know that you can do that with external program (for example systemd watchdog) to make it even more “bulletproof”. I have implemented that partially in systemd library for Erlang (now I see that I should add healthcheck function as well, will do in version 0.6.0).
Bonus hack:
I recently wrote a function to do the following: look for a config variable, possibly under a namespace, in the given
opts
as well as the Application env. I’ve written this at least six times before, and none of them looked great if they could handlefalse
values. Until I happened upon this lovely, somewhat cockeyed use ofwith
:Oh that’s gross…I love it.
If I’m reading this correctly, it works by assuming that the clauses will return
nil
, and when one of them successfully does, it treats it as a matching error and implicitly returns it.Normally you’d write something like:
It makes sense once you start thinking of
with
not in terms of success or failure, but in terms of match or no-match. If any of thewith
clauses return non-nil, that value is returned immediately. It is surprising at first, but it’s so much less ugly than nestedcase
s!I like your explanation. I am uncomfortable with that view but I see the utility of it. :)
“That’s gross… I love it” is roughly the sentiment I got in code review :D
Couple of (to some heretical) thoughts re:
with
.case
andwith
let you use guard clauses, which is handy.with
chain to make error handling easier:with {:step1, :ok} <- {:step1, do_thing()}
can then let you match on{:step1, error} -> handle_step1_error(error)
in theelse
branch.else
branch in yourwith
statements to show you’ve considered all error conditions.|>
is usually better handled viawith
due to error handling.case
statement can be faster but harder to maintain.else
clauses feels like a smell, and having to crap up yourwith
clauses with all those tuples starts to look worse than breaking eachwith
clause into its owncase
and just having one function call another upon success. Logically, I like keeping the contents of eachwith
clause paired with its error handling code more than I like stirring it all together.“letting it crash” is one of my triggers at this point. A lot of people seem to have really weird ideas about what that means. :-\
Heh, I kid. I recently wrote a module that runs some periodic health checks inside one of our services (run queue depth is the most relevant one) and restarts the VM upon failure. I’m not just letting it crash, I’m making damn sure of it now!
But you know that you can do that with external program (for example
systemd
watchdog) to make it even more “bulletproof”. I have implemented that partially insystemd
library for Erlang (now I see that I should add healthcheck function as well, will do in version 0.6.0).What do most people think it means? And what do you think it means?