View Source Control Flow
Elixir offers four control flow structures: if/unless
, case
, cond
, and with
.
if/2
and unless/2
If-else works as you'd expect. If your condition evaluates to truthy
(not nil
or false
), the code block is executed and the result is returned. If the condition evaluates to falsy
(nil
or false
), the else
block is executed if it exists; otherwise, nil
is returned.
The reverse form of if/2
is unless/2
. If your unless
condition evaluates to truthy
, the else
block is executed. If the condition is falsy
, the first block is executed. It is common to choose if
over unless
because it doesn't make your brain hurt, unless you want to execute a side effect or throw an exception.
age = 17
result =
if age >= 18 do
:adult
else
:minor
end
IO.inspect(result, label: 1)
unless age >= 18 do
raise "A minor tries to access the system!"
end
1: :minor
** (RuntimeError) A minor tries to access the system!
case/2
The case
statement is similar to switch
in other languages. It allows you to state multiple clauses and tries to match your conditional against each clause.
active_status = :active
fun =
fn value ->
case value do
%{status: ^active_status} -> :active
%{status: _other_status} -> :inactive
%{age: nil} -> :age_missing
%{age: age} when is_integer(age) and age >= 18 -> :adult
%{age: age} when is_integer(age) -> :minor
# An optional fallback which always matches.
# Without it, the case statement would raise an exception if no clause matches.
_ -> :default
end
end
fun.(%{status: :active}) # => :active
fun.(%{status: :foobar}) # => :inactive
fun.(%{age: nil}) # => :age_missing
fun.(%{age: 18}) # => :adult
fun.(%{age: 17}) # => :minor
fun.(nil) # => :default
cond/1
The cond
construct allows you to evaluate multiple clauses and return from the first that evaluates to truthy
. It is especially useful if you need to execute a function in a clause.
adult? = fn age -> age >= 18 end
fun =
fn value ->
cond do
value == :active -> :active
is_integer(value) && adult?.(value) -> :adult
is_integer(value) -> :minor
# The optional fallback clause must always evaluate to a truthy value.
# If you don't give this and no other clause matches, Elixir raises an exception.
true -> :default
end
end
fun.(:active) # => :active
fun.(18) # => :adult
fun.(17) # => :minor
fun.(nil) # => :default
with/1
The with/1
statement is useful to return early from a sequence of steps if one step fails. If you find yourself writing nested if-else or case statements, you probably want to use with/1
instead.
adult? = fn age -> age >= 18 end
details = %{name: "Peter", age: 32}
# Instead of writing nested if- or case-statements like this:
check_access = fn details ->
case details do
%{age: age} ->
if adult?.(age) do
:ok
else
{:error, :not_adult}
end
_ ->
{:error, :age_missing}
end
end
# Rather use one with-statement like this:
check_access_refactored = fn details ->
with %{age: age} <- details,
true <- adult?.(age) do
:ok
else
# Gets executed if any of the matches above fails.
%{} -> {:error, :age_missing}
false -> {:error, :not_adult}
end
end
To match inside the else
-block isn't great though. What if two matches can return false
? How would you know which match failed?
It is good practice to move steps step into small helper functions. This allows you to return a specific error message depending on which step failed and it makes the with
-statement more readable. It also makes it easy to see the "happy path" of your function and all its error cases.
defmodule RunElixir.Permissions do
def check_access(details) do
with {:ok, age} <- get_age(details),
:ok <- check_adult(age) do
:ok
end
end
# Moving each step into a small helper function gives you the flexibility
# to decide which error to return inside the function.
defp get_age(details) do
case details do
%{age: age} when is_integer(age) -> {:ok, age}
%{age: _age} -> {:error, :age_invalid}
_ -> {:error, :age_missing}
end
end
defp check_adult(age) do
if age >= 18, do: :ok, else: {:error, :not_adult}
end
end
RunElixir.Permissions.check_access(%{age: 32}) # => :ok
RunElixir.Permissions.check_access(%{age: nil}) # => {:error, :age_invalid}
RunElixir.Permissions.check_access(%{name: "Peter"}) # => {:error, :age_missing}
RunElixir.Permissions.check_access(%{age: 17}) # => {:error, :not_adult}