Control flow expressions are not used as often in Elixir as with more imperative languages mainly because controlling execution flow can be handled with a mix of pattern-matching, multi-clause functions and guard clauses.
And though using control flow expressions may be easier to understand at first, as the complexity of a program increases, nested control structures can start to creep in. Then we can simplify our code using the more functional patterns mentioned earlier.
There will definitely be times when you need to rely on Control Flow Expressions so it’s worth understanding how they work.
Here’s what we’ll cover:
-
if
/unless
-
cond
-
case
-
with
Versions
- Elixir 1.10.0
if and unless
If you’re familiar with Ruby, if and unless are used in the same way in Elixir.
if
if 5 > 1 do
IO.puts "5 is greater than 1"
else
IO.puts "5 is less than 1"
end
5 is greater than 1 so the expression is true.
unless
unless
will only execute the block when the expression results in nil or false:
unless 5 > 1 do
IO.puts "5 is less than 1"
else
IO.puts "5 is greater than 1"
end
Here the expression 5 > 1
is neither nil nor false so the else clause is executed.
NOTE: it’s recommended to avoid using else with unless. Prefer if / else.
Inline
if and unless can also be used with the inline notation:
iex> if 5 > 1, do: true, else: false
true
iex> unless 5 > 1, do: false, else: true
true
The else clause is optional. These examples will return nil if the main clause is falsy.
cond
The cond
structure is known as a multi-way if statement where the code associated with the first truthy condition is executed.
This can be used as a replacement for if / else if statements.
Here’s an example:
iex> x = 1
cond do
x > 1 -> "x is greater than 1"
x == 1 -> "x is equal to 1"
true -> "x is less than 1"
end
"x is equal to 1"
In this example, 1 is equal to 1 so the output was expected.
The last condition true serves as a default and will run when no other conditions are truthy.
case
case
accepts an expression and works with the return value of the expression.
It will pattern-match against the result from top to bottom running through the given patterns. Once a match is found, the code associated with it will run.
Let’s start with a simple example:
case 3 do
1 -> "one"
2 -> "two"
3 -> "three"
_ -> "did not find a match!"
end
"three"
Case Expression Result
Because case is an expression itself, it’s designed to return a value.
File.read/1
returns {:ok, binary}
, where binary is a binary data object that contains the contents of path, or {:error, reason}
if an error occurs.
Here’s an example:
result = case File.read("data_file.txt") do
{:error, reason} -> "The file could not be read",
{:ok, data} -> "#{length(data)} bytes were read from the file}
end
We can now use the result of the case statement in our program. Another contrived example, but it demonstrates that case is an expression.
with
with
was introduced in Elixir 1.2 and can be used when a series of expressions need to be successful.
with
can also be used to replace nested case instances or even a group of multi-clause functions.
Here are some important flow concepts to understand when using with:
- It accepts one or more expressions, a do block and an optional else clause.
- It will pattern match on the return value of each expression and will only run the code in the do block when every pattern matches.
- If one of the expressions do not have a match, it will return that expression’s value.
- If there is an else clause, this value will be return instead.
Let’s use a real world example shared on twitter by @devoncestes simplified for readability:
def create_subscription(email, other_args) do
with {:ok, user} <- lookup_user(email),
{:ok, customer} <- create_customer(user),
{:ok, subscription} <- create_subscription(customer),
:ok <- update_user(user) do
{:ok, :subscription_created}
end
end
Let’s walk through this function remembering that each expression must have a match before the code in the do block will run:
- a user must be found
- a customer must be created
- a subscription must be created
- the user must be updated successfully
If every expression has a match the function return value will be: {:ok, :subscription_created}
Notice that no else clause was given. This means that if any given expression doesn’t have a successful match, its return value will be returned.
Wrapping Up
We’ve discussed the main control flow expressions in Elixir and used some examples to explore how they might be used.
Though it can be better to use a more functional programming style with multi-clause functions et al, there will inevitably be situations where using if / unless, cond, case or with makes more sense.