Flow control structures consist of looping and branching constructs. Flow control structures such as if, while, and for are very common and essential in imperative code, and when learning imperative languages, they're one of the first things we learn. In Elixir, however, they are far less essential, and aren't necessary to learn at the very beginning.

Flow control structures aren't as essential in Elixir because they are used far less. Pattern matching, guard clauses, and multiple function clauses allow Elixir code to do a lot without using any flow control structures. What can be typically be done with a flow control structure in a function can also usually be done with multi-clause functions, which remove the need for these structures.

Elixir code, being functional, tends to declare what should happen rather than how it should happen, so flow control structures are used far less than in imperative programming. If you find yourself using flow control structures everywhere, you may be using an imperative way of thinking instead of a functional way of thinking. I know that it's certainly been difficult for me to break out of the imperative mindset that I've gotten used to over the years. So far in learning Elixir, I have not often missed having flow control structures available to me. It wasn't until I started learning about them did I even notice that I hadn't seen any so far.

So while flow control structures certainly aren't as essential in Elixir, they do exist, and they are used. They just aren't as common as in other languages. My recommendation based on the code I've seen so far is to not be afraid to use flow control structures, but don't overuse them. Overuse is typically a symptom that you're thinking imperatively rather than functionally.

Case

The case structure provides flow control using pattern matching where different blocks of code are run depending on which pattern is matched.

Here's an example of a hypothetical file read.

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

File.read/1, which we'll cover in more detail when I eventually learn more about how file I/O works, returns a tuple. That tuple either contains the :error atom and an explanation of what the error is or it contains the :ok atom and the data from the file when the file was read successfully.

In the code above, when there's an error, the tuple matches the first line and the case expression evaluates to the corresponding error message. If the file was read successfully, the tuple matches the second line and the case expression evaluates to a message indicating the number of bytes that were read.

We could have also done the same thing without the case expression by passing the result of File.read/1 to another function with two function clauses, which would be the purely functional way of doing this, but a case expression is another way of doing the same thing. This works since the expression is very simple. If there were significantly more complexity, then that would best be done in another function rather than in a case expression. It's a judgement call regarding what is more readable and maintainable.

The code to be run when a pattern is matched in a case expression can be a single line or it can be multiple lines. However, if the case expression gets too large, then it would be best to move the code to its own function rather than making it part of the case expression.

Like with places in Elixir that allow pattern matching, guard clauses can be added as well. Here's an example.

result = case person_data do
	{name, age} when age <= 30 ->
		age_in_5_years = age + 5
		{name, age_in_5_years}
	{name, age} when age > 30 ->
		age_5_years_before = age - 5
		{name, age_5_years_before}
end

When the person's age is 30 or less, the age has a 5 added to it. When the age is above 30, the age has 5 subtracted from it.

Here's what it looks like in IEx

iex> person_data = {"Hans", 88}
{"Hans", 88}
iex> result = case person_data do
...>    {name, age} when age <= 30 ->
...>            age_in_5_years = age + 5
...>            {name, age_in_5_years}
...>    {name, age} when age > 30 ->
...>            age_5_years_before = age - 5
...>            {name, age_5_years_before}
...> end
{"Hans", 83}

However, if I were writing this code, I would probably have moved it into its own function with separate guard clauses.

def get_modified_age({name, age}) when age <= 30 do
	age_in_5_years = age + 5
	{name, age_in_5_years}
end

def get_modified_age({name, age}) when age > 30 do
	age_5_years_before = age - 5
	{name, age_5_years_before}
end

I think it's a lot easier to understand and maintain that way. If this code needed to be in any way reusable, a function would be the way to go. From what I understand, using function clauses are considered to be preferable to a case expression.

What happens if there is no pattern match for a case expression? Let's find out.

iex> number = 3
3
iex> case number do
...>    5 -> "five"
...>    10 -> "ten"
...> end
** (CaseClauseError) no case clause matching: 3

It turns out that it throws an error similar to the error when function clauses do not match.

If you want a case clause that is run if the pattern does not match any of the previous clauses (like an else if an if-else statement), then you just need to do the same thing you would do if there were function clauses: use the wildcard character.

iex> number = 3
3
iex> case number do
...>    5 -> "five"
...>    10 -> "ten"
...>    _ -> "another number"
...> end
"another number"

The case statement is in many ways analagous to a function with multiple clauses. Like with pattern matching for functions, the clauses will be evaluated top to bottom, so more specific patterns should precede more generic patterns.

You could say that a case expression is similar to a switch/case statement in some imperative languages, but with pattern matching instead of logical comparisons or constants.

I'm not yet experienced enough with Elixir to be able to have a strong sense of when to use a case expression. All I can say is give preference to functions, pay attention to how it's used in existing Elixir code, and use your judgement.

Cond

The cond expression is much like the case expression, except that it uses logical comparisons instead of pattern matching. It looks a bit like a switch/case statement in imperative languages, but with boolean expressions instead of constants. It's also similar to an "if"-"else if"-"else" statement, but it's not exactly the same.

The cond expression consists of one or more clauses containing boolean expressions and the code to run when an expression evaluates to true. The cond expression evaluates the boolean expressions from top to bottom and runs the code associated with the first expression that evaluates to true. Unlike a case expression, there is no data in the first line of the expression that is being compared against the expressions below. A cond expression just consists of a list of boolean expressions.

Here's an example.

speed_evaluation = cond do
	car.speed <= 10 -> "crawling"
	car.speed <= 30 -> "sluggish"
	car.speed <= 60 -> "moderate"
	car.speed <= 100 -> "speedy"
	car.speed > 100 && car.speed < 200 -> "super fast"
	true -> "either a car going in reverse or a jet"
end

If the speed of the car was 45, for example, the cond expression would evaluate to "moderate". The true clause is the equivalent of an else in an if statement or default: for a switch statement in C# and Javascript. It always evaluates to true, and will be run if none of the other expressions evaluate to true. In the example above, the true clause would only be evaluated if the car's speed is greater than 200 or if it is negative.

If none of the expressions in a cond expression evaluate to true, Elixir will raise a CondClauseError. So you can choose if you want to handle all possible cases or you can let the code fail with an error if an unexpected situation is encountered.

If

An if expression is very similar to other languages, except that there is no equivalent of an "else if". It's just a binary decision rather than an N-nary decision.

Here is an example.

if age > 18 do
	"adult"
else
	"child"
end

Let's see the same thing in IEx

iex> age = 8
8
iex> if age > 18 do
...>    "adult"
...> else
...>    "child"
...> end
"child"

If you want "else if" functionality, use a cond expression instead.

An if expression can also be written on one line

iex> age = 40
40
iex> category = if age > 18, do: "adult", else: "child"
"adult"

Unless

An unless expression looks just like an if expression, except that the if is replaced by unless. Whereas if will evaluate to the first expression if the boolean condition is true, unless evaluates to the first expression if the boolean expression is false.

Here's the same example from above rewritten using unless.

unless age > 18 do
	"child"
else
	"adult"
end

Here it is in IEx.

iex> age = 10
10
iex> unless age > 18 do
...>    "child"
...> else
...>    "adult"
...> end

An unless with an else tends to be harder to understand than the if with an else because it's inverted from the normal way of thinking, so it's more typically used without an else.

For example, the following code will allow entrance to anyone who isn't named "Bob". Sorry, Bob, you're out of luck.

unless name == "Bob" do
	allow_entrance()
end

Here's a demonstration in IEx.

iex> name = "Frederick"
"Frederick"
iex> unless name == "Bob" do
...>    allow_entrance.()
...> end
"allowing entrance"
iex> name = "Bob"
"Bob"
iex> unless name == "Bob" do
...>    allow_entrance.()
...> end
nil

Those are all the flow control statements Elixir has. Notice that there were no loop structures mentioned here. That's because Elixir does not have any: loops are an imperative concept. Elixir accomplishes the same thing through recursion.