Control Flow
Mix.install([
{:kino, github: "livebook-dev/kino", override: true},
{:kino_lab, "~> 0.1.0-dev", github: "jonatanklosko/kino_lab"},
{:vega_lite, "~> 0.1.4"},
{:kino_vega_lite, "~> 0.1.1"},
{:benchee, "~> 0.1"},
{:ecto, "~> 3.7"},
{:math, "~> 0.7.0"},
{:faker, "~> 0.17.0"},
{:utils, path: "#{__DIR__}/../utils"}
])
Navigation
Setup
Ensure you type the ea
keyboard shortcut to evaluate all Elixir cells before starting. Alternatively you can evaluate the Elixir cells as you read.
Overview
Control flow. What does that mean?
Well, so far you’ve learned mostly about how to give computers a single set of instructions. But sometimes, depending on certain conditions, you want to deliver a different set of instructions.
flowchart
Input --- Condition
Condition --- 1[Instruction]
Condition --- 2[Instruction]
The flow of our program through these branches, and how we control it is called control flow. We have a variety of tools to control the flow of our program.
Let’s say you’re building a rock paper scissors game. You’re familiar with the rules.
- Rock beats Scissors
- Paper beats Rock
- Scissors beats Paper
Player1 will input "Rock"
, "Paper"
, or "Scissors"
. Then player2 will do the same.
then the program will return either "player1"
or "player2"
depending on who won.
flowchart LR
Rock --> Function
Paper --> Function
Scissors --> Function
Function --> Player1
Function --> Draw
Function --> Player2
The branching arrows represent a different flow that our program might take. Depending on the input the player will either win, lose, or draw.
So how would you implement a game like this? Well, you might try to use operators. We’ll start by assuming player2 always chooses rock.
defmodule RockPaperScissors do
def play(player1_choice) do
(player1_choice === "Rock" && "Draw") ||
(player1_choice === "Paper" && "Player1") ||
(player1_choice === "Scissors" && "Player2")
end
end
RockPaperScissors.play("Scissors")
That worked, but what about when we try to accept input from player 2?
defmodule RockPaperScissors do
def play(player1_choice, player2_choice) do
(player1_choice === "Rock" && player2_choice === "Rock" && "Draw") ||
(player1_choice === "Rock" && player2_choice === "Paper" && "Player2") ||
(player1_choice === "Rock" && player2_choice === "Scissors" && "Player1") ||
(player1_choice === "Paper" && player2_choice === "Rock" && "Player2") ||
(player1_choice === "Paper" && player2_choice === "Paper" && "Draw") ||
(player1_choice === "Paper" && player2_choice === "Scissors" && "Player2") ||
(player1_choice === "Scissors" && player2_choice === "Rock" && "Player2") ||
(player1_choice === "Scissors" && player2_choice === "Paper" && "Player1") ||
(player1_choice === "Scissors" && player2_choice === "Scissors" && "Draw")
end
end
RockPaperScissors.play("Rock", "Rock")
Woah. Ok, that’s a bit overwhelming and unclear. For example. I’ve purposely left a bug in the code above. Can you find it?
Your Turn
Find the bug in the code above and call the module function with the input that will trigger it.
Spoiler It’s “Paper” and “Rock”. player1 should win, but instead player2 wins.
Clear Control Flow
Through the example above, you’ve learned that clear control flow is important. It’s not enough to have a program work, it should also be clear to understand.
So, control flow is the branching flow of decisions in our program. In the rock paper scissors game above it’s the following.
flowchart
A[Rock] --> 1[Rock] --> a[Draw]
A[Rock] --> 2[Paper] --> b[Lose]
A[Rock] --> 3[Scissors] --> c[Win]
B[Paper] --> 4[Rock] --> d[Win]
B[Paper] --> 5[Paper] --> e[Draw]
B[Paper] --> 6[Scissors] --> f[Lose]
C[Scissors] --> 7[Rock] --> g[Lose]
C[Scissors] --> 8[Paper] --> h[Win]
C[Scissors] --> 9[Scissors] --> i[Draw]
Often the complexity of a program is determined by the number of branching decisions it needs to make.
flowchart
A[Input] --> B[Decision]
B --> B1[Decision]
B --> B2[Decision]
B --> B3[Decision]
B1 --> B11[Decision]
B1 --> B12[Decision]
B1 --> B13[Decision]
B2 --> B21[Decision]
B2 --> B22[Decision]
B2 --> B23[Decision]
B3 --> B31[Decision]
B3 --> B32[Decision]
B3 --> B33[Decision]
You can reduce the complexity of a program by reducing the number of branching decisions. For example, what if we reduced Rock Paper Scissors to the following?
flowchart LR
a[choice1] --> b[beats?] --> c[choice2] --> d[Player1]
c --> or --> 1[choice2] --> 2[beats?] --> 3[choice1] --> 4[Player2]
3 --> 5[or] --> Draw
Then beats?
could be its own piece of control flow that returns a boolean.
flowchart
Input --> A[Rock, Scissors] --> true
Input --> B[Paper, Rock] --> true
Input --> C[Scissors, Paper] --> true
Input --> D[Other] --> false
Converting that into code, we would get the following. Readability is sometimes subjective, but we can probably agree that this is easier to understand.
defmodule RockPaperScissors do
def play(player1_choice, player2_choice) do
(beats?(player1_choice, player2_choice) && "Player1") ||
(beats?(player2_choice, player1_choice) && "Player2") ||
"Draw"
end
def beats?(choice1, choice2) do
(choice1 === "Rock" && choice2 === "Scissors") ||
(choice1 === "Paper" && choice2 === "Rock") ||
(choice1 === "Scissors" && choice2 === "Paper")
end
end
RockPaperScissors.play("Paper", "Scissors")
To further handle the complexity of control flow, Elixir provides a number of control flow structures that make our job easier.
In this lesson, we’ll cover:
-
Using
if
andunless
to handle control flow with two paths. -
Using
case
to create many branching flows depending on a single input. -
Using
cond
to create many branching flows depending on many inputs.
If
In the real world, you use if all the time.
- If it is rainy then I will stay inside
- If it is sunny then I will go outdoors
if
means exactly what it says. It checks for some condition, then executes some
action.
In Elixir, we provide if a boolean or a truthy value, and then execute the block of code inside of it.
if true do
"Hello!"
end
However, if we pass if
nil
or false
, then it will not execute the code inside.
if false do
"Hello"
end
Passing if a literal true
or false
value isn’t useful. Anytime you do that you know
you’ve probably made a mistake in your code.
Why?
Because, you already you wanted to execute the code inside the if
or not, so you should omit
the if
statement.
Instead, it’s more useful to check if some condition is true.
Your Turn
Here’s a little program that checks the current hour of the day and displays either "Good morning!"
or "Good afternoon!"
.
Replace 8
with a time in the afternoon using 24 hour format. See that it now runs "Good afternoon!"
current_hour = 8
if current_hour < 12 do
"Good morning!"
end
if current_hour >= 12 do
"Good Afternoon!"
end
Else
We can also use else
with if to create two branching paths.
The code inside of the else
will execute if the condition in the if
statement is false
or nil
.
Keep in mind that else allows for every other condition, so it can sometimes create unexpected results.
current_hour = 13
if current_hour >= 12 do
"Good Afternoon!"
else
"Good Morning!"
end
If statements are really good at splitting the control flow in two, but they aren’t very good at splitting it further.
In some programming languages there is an else if
construct which allows you to create many
conditions. However Elixir relies on other constructs which you will learn about further on in this
lesson.
It’s usually unclear to nest if statements and a sign that you can improve your code.
condition1 = true
condition2 = true
if condition1 do
if condition2 do
{true, true}
end
end
Instead, you can often group conditions together, or rely on a different construct than if
.
if condition1 and condition2 do
{true, true}
end
Your Turn
Let’s create a thermometer program. This thermometer program will take a temperature in and return
"hot"
or "cold"
.
To avoid debate and confusion over Celsius and Fahrenheit. Any number greater than or equal
to 20
will be hot.
Enter your answer in the Elixir cell below.
Unless
unless
is if
in reverse. It’s helpful for times when you want to always do something unless
some condition is true.
For example:
- Unless it rains on sunday lets go to the park.
- Unless it is the weekend you work.
- Unless it’s high tide, lets go to the beach.
unless false do
"Hello!"
end
unless true do
"Hello!"
end
is_raining = false
unless is_raining do
"Let's go to the beach!"
end
You can also use else
with unless but it’s not always the most clear to read and should probably
be an if
instead.
condition = true
unless condition do
else
"Will I print?"
end
Your Turn
In the Elixir cell below
-
Create an variable named
tired
which istrue
orfalse
. -
Create an
unless
statement. which returns"awake"
unlesstired
.
Case
Case is often sometimes called switch case
in other languages.
It’s a control flow structure that allows you to define a series of cases.
For example:
- Case 1: It’s sunny so wear a t-shirt.
- Case 2: It’s rainy so wear a rain jacket.
- Case 3: It’s cold so wear a sweater.
- Case 4: It’s snowing so wear a thick coat.
flowchart
case --> 1
case --> 2
case --> 3
case --> 4
1[sunny] --> A[wear a t-shirt]
2[rainy] --> B[wear a rain jacket]
3[cold] --> C[wear a sweater]
4[snowing] --> D[wear a thick coat]
To use a case statement start with the case
keyword.
case
Then enter a value that will match one of the cases. We’ll use “sunny” from above.
case "sunny"
Now write the do
keyword to start defining the series of potential cases.
case "sunny" do
Start defining the “sunny” case
case "sunny" do
"sunny"
Separate the “sunny” case and what will happen with the value is “sunny” using ->
case "sunny" do
"sunny" ->
Enter what will happen when it’s the "sunny"
case. "wear a t-shirt"
.
case "sunny" do
"sunny" -> "wear a t-shirt"
end
the case statement.
case "sunny" do
"sunny" -> "wear a t-shirt"
end
That gives us the following case statement.
case "sunny" do
"sunny" -> "wear a t-shirt"
end
You can create more cases as well. Let’s fill in the rest of the examples from above. It’s
still going to return the "sunny"
case though.
case "sunny" do
"sunny" -> "wear a t-shirt"
"rainy" -> "wear a rain jacket"
"cold" -> "wear a sweater"
"snowy" -> "wear a thick coat"
end
Your Turn
If the value between case
and do
changes, it will hit a different case.
try changing the weather
variable by binding it to "rainy"
and re-evaluate the
Elixir cell. Try that again with "cold"
, and "snowy"
.
weather = "sunny"
case weather do
"sunny" -> "wear a t-shirt"
"rainy" -> "wear a rain jacket"
"cold" -> "wear a sweater"
"snowy" -> "wear a thick coat"
end
Case Visualization
Under the hood, Elixir is checking the value given to the case statement, and then checking if it matches any of the cases. It then executes the first case that matches.
Utils.slide(:case)
Now, you may notice that if you give the case
statement a value that doesn’t match any case,
the program crashes with a CaseClauseError error. In general, it’s wise to have a default
case that handles any value.
case "no match" do
"sunny" -> "wear a t-shirt"
end
To provide a default case for the case
statement, you use an underscore _
.
case "no match" do
"sunnny" -> "wear a t-shirt"
_ -> "wear clothing"
end
Cond
cond
stands for condition. It allows you to have many different conditions and branches.
It’s unlike case in that instead of checking a single value, it can compare many, and check for more complex conditions.
For example:
- Condition 1: the plant wilting and it’s dark -> use a UV light.
- Condition 2: the plant is wilting and it’s sunny -> put the plant in sunlight.
- Condition 3: It’s been 2 weeks since you watered the plant -> water the plant.
- Condition 4: the plant is dead -> get a new plant.
flowchart
cond --> 1
cond --> 2
cond --> 3
cond --> 4
1[the plant wilting and it's dark] --> A[use a UV light]
2[the plant is wilting and it's sunny] --> B[ put the plant in sunlight]
3[It's been 2 weeks since you watered the plant] --> C[water the plant]
4[the plant is dead] --> D[get a new plant]
Notice that instead of cases based on a single value, we have conditions based on multiple values such as the state of the plant, time, and weather.
The syntax for cond
looks faily similar to case, except there’s no value. Instead you give
cond
a truthy or falsy value on the left of the ->
.
cond do
true -> "good morning!"
end
true
is the default condition in cond
. You can use operators on the left-hand side
of the ->
and whichever condition returns true
first will execute.
Let’s convert the example above for plants into code.
daylight = true
days_since_watered = 14
plant = "wilting"
cond do
plant === "wilting" && !daylight -> "use a UV light"
plant === "wilting" && daylight -> "put the plant in sunlight"
days_since_watered >= 14 -> "water the plant"
plant === "dead" -> "get a new plant"
end
cond
will execute the instructions for the first condition that returns true. You often have to
be careful of order. For example, there’s a bug in the above condition.
Right now, if it’s been 14 days since we watered the plant, we’ll always water it. But if the
plant is dead that doesn’t really make sense. Instead, we should move the plant === "dead"
check
to be the highest in priority.
daylight = true
days_since_watered = 14
plant = "dead"
cond do
plant === "dead" -> "get a new plant"
plant === "wilting" && !daylight -> "use a UV light"
plant === "wilting" && daylight -> "put the plant in sunlight"
days_since_watered >= 14 -> "water the plant"
end
Let’s follow the flow of execution to try to make this more clear.
Utils.slide(:cond)
Your Turn
In the Elixir cell below:
-
Create a variable called
grade
which will be a number grade from 1 to 100. -
Create a condition that returns
"A"
,"B"
,"C"
, or"D"
depending on the value ofgrade
.
The conditions for grade
should be:
- 85-100 is an A
- 70-84 is a B
- 55-59 is a C
- 1-54 is a D
Commit Your Progress
Run the following in your command line from the project folder to track and save your progress in a Git commit.
$ git add .
$ git commit -m "finish control flow section"