Many Elixir functions return tagged tuples:
{:ok, value} on success and {:error, reason} on
failure. with chains several such steps and stops
at the first one that doesn't match.
1
defmoduleSignupdodefparse_age(input)docaseInteger.parse(input)do{age,""}->{:ok,age}_->{:error,:not_a_number}endenddefcheck_adult(age)whenage>=18,do:{:ok,age}defcheck_adult(_age),do:{:error,:too_young}defcheck_email(email)doifString.contains?(email,"@"),do:{:ok,email},else:{:error,:bad_email}end# Each `pattern <- expression` clause must match to# continue. The `do` block runs only if all match.defrun(params)dowith{:ok,age}<-parse_age(params["age"]),{:ok,age}<-check_adult(age),{:ok,email}<-check_email(params["email"])do{:ok,%{age:age,email:email}}endendend
else lets you translate failures, for example
into messages for a user. Its clauses match the
value that broke the chain.
4
explain=fnparams->with{:ok,age}<-Signup.parse_age(params["age"]),{:ok,_}<-Signup.check_adult(age)do"welcome"else{:error,:not_a_number}->"age must be a number"{:error,:too_young}->"come back when you're 18"endendIO.puts(explain.(%{"age"=>"40"}))IO.puts(explain.(%{"age"=>"x"}))IO.puts(explain.(%{"age"=>"9"}))
welcome
age must be a number
come back when you're 18
Plain = matches work inside with too, for
intermediate values that can't fail.
Pitfall: a long else that matches every
possible error from every step couples the caller
to all of them. It's usually better to make each
step return a descriptive {:error, reason} and
let it pass through untouched.
Pitfall: if no else clause matches, with
raises WithClauseError. Once you add else,
cover every failure shape the steps can return.