TaggedWith
A with that tags each clause's failure by name, so else can tell which
step failed even when two clauses can fail with the same-looking value.
Plain with matches else on whatever shape a clause's expression happens
to return. That breaks down when two clauses can produce the same-looking
value — say, validate_email/1 and validate_age/1 both returning
{:error, :invalid}. A pattern like {:error, :invalid} in else can't
tell you which one failed.
Tagging each clause fixes that — just {name, pattern} <- {name, expr} —
but it means writing the name twice per clause, by hand, every time:
# what you'd otherwise write by hand
with {:email, :ok} <- {:email, validate_email(params)},
{:age, :ok} <- {:age, validate_age(params)} do
:ok
else
{:email, {:error, reason}} -> {:error, {:invalid_email, reason}}
{:age, {:error, reason}} -> {:error, {:invalid_age, reason}}
end
# what you actually write
import TaggedWith
tagged_with email: :ok <- validate_email(params),
age: :ok <- validate_age(params) do
:ok
else
{:email, {:error, reason}} -> {:error, {:invalid_email, reason}}
{:age, {:error, reason}} -> {:error, {:invalid_age, reason}}
end
Each clause is name: pattern <- expr — no parens needed around
pattern <- expr, it parses fine as a keyword value. Write the bare
pattern you'd use in a plain with (:ok, {:ok, value}, ...) —
tagged_with adds the name to both sides for you, expanding into exactly
the hand-written form above, so else can still catch and tell apart the
failure from each line.
else is optional, exactly like in plain with. Without it, a failed
clause's tagged value (e.g. {:email, {:error, reason}}) comes back
directly — no exception, no extra behavior, since that's already how
with behaves without else.
Installation
Add tagged_with to your list of dependencies in mix.exs:
def deps do
[
{:tagged_with, "~> 0.1.0"}
]
end