with clause is used to combine matching clauses.
It looks like we combine anonymous functions or handle function with multiple bodies (matching clauses).
Consider the case: we create a user, insert it into DB, then create greet email and then send it to the user.
Without the with clause we might write something like this (I omitted functions implementations):
case create_user(user_params) do
{:ok, user} ->
case Mailer.compose_email(user) do
{:ok, email} ->
Mailer.send_email(email)
{:error, reason} ->
handle_error
end
{:error, changeset} ->
handle_error
end
Here we handle our business process's flow with case (it could be cond or if). That leads us to so-called 'pyramid of doom', because we have to deal with possible conditions and decide: whether move further or not. It would be much nicer to rewrite this code with with statement:
with {:ok, user} <- create_user(user_params),
{:ok, email} <- Mailer.compose_email(user) do
{:ok, Mailer.send_email}
else
{:error, _reason} ->
handle_error
end
In the code snippet above we've rewrite nested case clauses with with. Within with we invoke some functions (either anonymous or named) and pattern match on their outputs. If all matched, with return do block result, or else block result otherwise.
We can omit else so with will return either do block result or the first fail result.
So, the value of with statement is its do block result.