Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Control Flow

control_flow.livemd

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

Return Home Report An Issue

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 and unless 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 is true or false.
  • Create an unless statement. which returns "awake" unless tired.

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" &amp;&amp; !daylight -> "use a UV light"
  plant === "wilting" &amp;&amp; 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" &amp;&amp; !daylight -> "use a UV light"
  plant === "wilting" &amp;&amp; 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 of grade.

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"