|
I've seen that recommendation often but I still dislike it, since it falls apart whenever you need to do the same logic on multiple pieces of data. It only makes sense if every single with-clause is doing something hugely different. A simple example: with {:ok, cleaned_first} <- sanitize(data.first),
{:ok, cleaned_middle} <- sanitize(data.middle),
{:ok, cleaned_last} <- sanitize(data.last),
{:ok, final} <- combine(first, middle, last) do:
{:ok, "Name is" <> final}
else
{:error, :illegal_character} ->
{:error, :illegal_first_name_or_middle_name_or_last_name}
other ->
other
end
The above sucks because it's not clear which data was bad or which step failed.So let's talk about the alternatives... __________________ First, the "Lots Of Methods" approach, where you just add as many different private methods as you need, where each one works almost the same except for a distinctly different error: {:ok, cleaned_first} <- sanitize_first(data.first),
{:ok, cleaned_middle} <- sanitize_middle(data.first),
{:ok, cleaned_last} <- sanitize_last(data.first),
I feel that sucks because it's tedious, error-prone boilerplate, even if they're just wrappers around a sanitize/1 that does the real work. Also any new error-atoms being introduced are scattered down the file rather than near where you might want to match/test them.__________________ Second, the "Augment One Method With Causal Data" version: {:ok, cleaned_first} <- sanitize(data.first, :phase_first),
# ... else ...
{:error, :illegal_character, :phase_first} -> {:error, :illegal_first_name}
This is a marginal improvement, but we're still contaminating methods like "sanitize" with junk they don't actually need to know in order to do so their job, passing a piece of opaque data down and up the stack unnecessarily.__________________ Third, what if we carefully isolate the concerns/complexity to the with-statement... Hey! We're right back to where we started! The "Augment The With Clauses" approach, which I argue is least-bad: {_phase, {:ok, cleaned_first}} <- {:phase_first, sanitize(data.first)},
{_phase, {:ok, cleaned_middle}} <- {:phase_middle, sanitize(data.middle)},
{_phase, {:ok, cleaned_last}} <- {:phase_last, sanitize(data.last)},
# ... else ...
{:phase_first, {:error, :illegal_character}} -> {:error, :illegal_first_name}
{:phase_middle, {:error, :illegal_character}} -> {:error, :illegal_middle_name}
{:phase_last, {:error, :illegal_character}} -> {:error, :illegal_last_name}
|