Elixir, as a functional programming language, normally follows declarative programming paradigm instead of imperative. By defining lots of small independent functions and use some tools Elixir provides us, we will use less of control flow constructions comparing to other languages. Well, maybe comparing to imperative languages.
However, we have tools to control a flow in Elixir and it is always good to know them.
Conditions
Let’s start with the most common one.
The if
Elixir provides us the if/2
macros which receive condition as a first argument and the keyword list as a second:
iex> if 1 < 2, do: "Yes. 1 is less than 2"
"Yes. 1 is less than 2"
We can also store a result of the expression into a variable:
iex> result = if 1 < 2, do: "Yes. 1 is less than 2"
"Yes. 1 is less than 2"
iex> result
"Yes. 1 is less than 2"
it supports else
argument as well.
iex> if 1 > 2, do: "Yes. 1 is less than 2", else: "No. It is not"
"No. It is not"
Some people might argue it is a ternary operator in Elixir. I’m not sure about that. Even though it is a one-liner, I would barely call it ternary.
Elixir also provides us a version sprinkled with a syntactic sugar:
iex> if 1 < 2 do
...> "Yes. 1 is less than 2"
...> end
"Yes. 1 is less than 2"
iex> if 1 > 2 do
...> "Yes. 1 is less than 2"
...> else
...> "No. It is not"
...> end
"No. It is not"
There is also a unless/2
macro, which stands for “if not”:
iex> unless 1 > 2, do: "Yes. 1 is less than 2"
"Yes. 1 is less than 2"
iex> unless 1 < 2, do: "Yes. 1 is less than 2", else: "No. It is not"
"No. It is not"
iex> unless 1 > 2 do
...> "Yes. 1 is less than 2"
...> end
"Yes. 1 is less than 2"
iex> unless 1 < 2 do
...> "Yes. 1 is less than 2"
...> else
...> "No. It is not"
...> end
"No. It is not"
I didn’t find the way to implement “if/elseif/else” condition using if/2
macros, thus I’m not sure that is possible.
Well, we can achieve it by placing one more if/2
inside else
block, but that would probably do not look nice.
Another way to do that is to use cond/1
macro.
The cond
Once we have no if/elseif/else
(well I think so), we can use cond/1
instead. Let’s solve the FizzBuzz problem using cond
.
defmodule FizzBuzz do
def calculate(n) do
cond do
rem(n, 15) == 0 -> # if
"FizzBuzz"
rem(n, 3) == 0 -> # else if
"Fizz"
rem(n, 5) == 0 -> # else if
"Buzz"
true -> # else
n
end
end
end
iex> c "fizz_buzz_cond.ex"
[FizzBuzz]
iex> Enum.map([1, 3, 5, 15, 16], &FizzBuzz.calculate/1)
[1, "Fizz", "Buzz", "FizzBuzz", 16]
I’ve put some comments just to reflect the comparison with if/elseif/else
from other programming languages.
You can also compare the implementation of FizzBuzz with the previous implementation of the problem without any conditions.
As we can see here, the syntax of cond/1
is pretty simple. We are passing a block which contains condition -> result
expressions. We can have them as much as we need.
Small side note cond/1
will raise an CondClauseError
exception if none of the conditions are satisfied.
iex> cond do
...> 1 == 2 -> "Nope"
...> end
** (CondClauseError) no cond clause evaluated to a true value
That is why we had true -> n
as a final else
in our FizzBuzz solution.
And the case
The syntax of the case/2
is very similar to the switch statements from any other programming languages.
We have an expression and we match it against other clauses we have:
iex> case 2 + 2 do
...> 5 -> "Nope"
...> 4 -> "Yep"
...> 22 -> "What?"
...> end
"Yep"
As well as cond/1
case/2
will raise an CaseClauseError
exception if there are no matches:
iex(1)> case 3 + 3 do
...(1)> 4 -> "Nope"
...(1)> end
** (CaseClauseError) no case clause matching: 1
That is why providing a default clause would be a wise idea.
iex> case 3 + 3 do
...> 4 -> "Nope"
...> _ -> "I don't care"
...> end
"I don't care"
Loops
Let’s look at another control flow statement as loops.
There are no loops in Elixir.
That’s it.
Done.
Ok. You probably expect some explanation here.
Due to immutability of its values, Elixir does not have loops in the usual for imperative languages form.
In Elixir (I think as in other declarative languages) the one way would achieve it using recursion.
defmodule Loop do
def puts_times(0, _message) do
# Recursion terminator
end
def puts_times(n, message) do
IO.puts message
puts_times(n - 1, message)
end
end
iex> c "loop.ex"
[Loop]
iex> Loop.puts_times(5, "Hello Loop!")
Hello Loop!
Hello Loop!
Hello Loop!
Hello Loop!
Hello Loop!
nil
Comprehensions
You may be familiar with for-each
loop from some other languages.
Which takes an array and goes through every element of that array and does something with the element.
For Ruby it would be for
loop:
irb> for i in 1..3 do
irb* puts i
irb> end
1
2
3
=> 1..3
Elixir provides us similar functionality (probably improved one), but here it is called “Comprehensions”. So not a loop again.
The similar example in Elixir looks like:
iex> for i <- 1..3, do: IO.puts(i)
1
2
3
[:ok, :ok, :ok]
Where the for/1
accepts “generator” to iterate through the values.
Same as for the if
condition we have a syntactic sugar version.
iex> for i <- 1..3 do
...> IO.puts(i)
...> end
1
2
3
[:ok, :ok, :ok]
The first difference we can see comparing to Ruby it is that comprehension returns a list containing the result of the operation instead of the initial collection.
In our case it’s 1..3
vs [:ok, :ok, :ok]
(:ok
is a result of IO.puts
).
Let’s suppose we have a collection of numbers but we want to work only with numbers less than 4. The naive implementation might look like:
iex> for i <- 1..5 do
...> if i < 4, do: i
...> end
[1, 2, 3, nil, nil]
As we can see that is not only naive but also does not work as we expected.
For these cases comprehensions also accepts filters.
iex> for i <- 1..5, i < 4, do: i
[1, 2, 3]
Now we got only permitted values as a result.
Next, let’s try to nest one comprehension into another.
iex> for i <- [1, 2] do
...> for j <- [11, 12] do
...> {i, j}
...> end
...> end
[[{1, 11}, {1, 12}], [{2, 11}, {2, 12}]]
In this case, we are going through every combination of i
and j
and return is as a tuple.
In case we want to avoid nested lists we would need to pass the result to List.flatten/1
function:
iex> List.flatten [[{1, 11}, {1, 12}], [{2, 11}, {2, 12}]]
[{1, 11}, {1, 12}, {2, 11}, {2, 12}]
It works, but I would say the implemtation does not look in Elixir way. Now let’s check how it can be done using single comprehension:
iex> for i <- [1, 2], j <- [11, 12], do: {i, j}
[{1, 11}, {1, 12}, {2, 11}, {2, 12}]
We can provide generators one by one and comprehension will iterate through every value and return the result as a one-level list.
The last thing I want to mention is: The variable assignments inside the comprehension are treated as local variables. That being said defining a variable with the same name does not affect the existing one.
iex> i = 503
503
iex> for i <- 1..3, do: i*i
[1, 4, 9]
iex> i
503
Summary
Let’s take a quick look what did we learn here.
Now we know how to control the flow of our programs even though Elixir provides us different alternatives.
We figured out how to use if
condition and how to implement if/elseif/else
conditions in Elixir using cond
macro.
We have learned that Elixir does not have loops in the traditional understanding of imperative languages and we need to use either recursions or comprehensions to iterate through collections.
We have covered almost all the basics of Elixir, but we will not stop here. We will go deeper into the world of Elixir in the following articles.