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

Control Flow

reading/control_flow.livemd

Control Flow

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.9", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"},
  {:smart_animation, github: "brooklinjazz/smart_animation"}
])

Navigation

Home Report An Issue FunctionsNaming Numbers

Review Questions

Upon completing this lesson, a student should be able to answer the following questions.

  • How do we execute code in our programs depending on some condition?
  • When might you use case vs cond vs if?
  • How can you use pattern matching inside of a case statement?

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 including if, cond, and case statements.

If Statements

We’ve already seen if statements, which are a useful when we have one or two branching paths in our code.

For example, we might build a weather app that provides recommendations of what to wear.

We’ll have some condition like if it is cold then provide a recommendation to wear a coat. If it is not cold, then we’ll provide a recommendation to wear a t-shirt.

flowchart LR
weather --> hot --> t-shirt 
weather --> cold --> coat

Here’s how we can translate that into a program.

weather = :cold

if weather == :cold do
  "coat"
else
  "t-shirt"
end

if statements are only effective for up to two paths. We’ll likely have to write overly verbose solutions that involve re-binding a variable if we try to use them for multiple conditions.

This is not recommended. For example, let’s expand our weather application to handle many conditions, instead of only hot and cold.

flowchart
weather --> sunny --> r1[t-shirt]
weather --> snowing --> r2[thick coat]
weather --> rainy --> r3[rain coat]
weather --> foggy --> r4[something bright]
# Change :sunny To :snowing, :raining, And :foggy To See A Different Result.
weather = :raining

recommendation = "t-shirt"
recommendation = if weather == :snowing, do: "thick coat", else: recommendation
recommendation = if weather == :raining, do: "raincoat", else: recommendation
recommendation = if weather == :foggy, do: "something bright", else: recommendation

recommendation

Your Turn

Create a thermometer program that determines if the temperature is :hot, or :cold. Bind a variable temperature to a number between 0 and 20. If the temperature is above 10, then return :hot, otherwise return :cold.

Example solution

temperature = 10

if temperature > 10 do
  :hot
else
  :cold
end

Enter your solution below.

temperature = Enum.random(0..20)

if temperature > 10, do: :hot, else: :cold

Clear Control Flow

Control flow impacts the complexity of a program. The more branching paths we have in our program, the more complex it is.

Some complexity is necessary, but overly complex programs are often the result of improper use of control flow.

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]

We can simplify our programs by reducing the number of branching decisions and leveraging different control flow structures.

Case

case is a control flow structure that allows you to define a series of cases, and handle them separately.

case is best used when you have many branching conditions, that all work well with pattern matching.

We’ll use this to improve our weather application that handles many different conditions.

flowchart
weather --> sunny --> r1[t-shirt]
weather --> snowing --> r2[thick coat]
weather --> rainy --> r3[rain coat]
weather --> foggy --> r4[something bright]

We can break this control flow down into several cases.

  • Case 1: It’s sunny so wear a t-shirt.
  • Case 4: It’s snowing so wear a thick coat.
  • Case 2: It’s raining so wear a raincoat.
  • Case 3: It’s foggy so wear a something bright.

Here’s how we translate that into code. This is significantly more straightforward than our solution using if.

# Change :sunny To :snowing, :raining, And :foggy To See A Different Result.
weather = :sunny

case weather do
  :sunny -> "t-shirt"
  :snowing -> "thick coat"
  :raining -> "raincoat"
  :foggy -> "something bright"
end

Case Breakdown

Let’s break down the case statement above. To use a case statement, start with the case keyword.

case

Then enter a value that will match one of the cases. This can be a variable, or any elixir term. We’ll use “sunny” from above.

case :sunny

Now write the do keyword to start defining the series of potential cases.

case :sunny do

Defining the “sunny” case. We’ll trigger the case that matches the value between case and do.

case :sunny do
  :sunny

We separate the pattern to match on with the code to run if it matches using the ->.

case :sunny do
  :sunny ->

We then provide the code to run when the case hits. In this case, "t-shirt".

case :sunny do
  :sunny -> "t-shirt"

Then we end the case statement.

case :sunny do
  :sunny -> "t-shirt"
end

We can provide any number of cases. Cases use pattern matching to decide which result to run.

Your Turn

Return each value from 1 to 5 by changing the condition variable below.

condition = Enum.random([:red, "blue", %{key: "value"}, 123, [1, 2, 3], :default])

case condition do
  :red -> 1
  "blue" -> 2
  %{key: "value"} -> 3
  123 -> 4
  [1, 2, 3] -> 5
  _ -> "default"
end

Pattern Matching With Case

case uses pattern matching to determine which case to run. If the left side of the -> operator would match with the provided value, then the case statement returns the right side of the -> operator.

Exact values always match.

case {:exactly, :equal} do
  {:exactly, :equal} -> "I hit this case"
end

Under the hood, Elixir checks if the condition, and the left side of the -> symbol would match.

{:exactly, :equal} = {:exactly, :equal}

If the values would throw a MatchError with the match operator =, then they do not match.

{:not_exactly, :equal} = {:exactly, :equal}

That’s why the following case statement does not trigger the "non-matching case".

case {:exactly, :equal} do
  {:not_exactly, :equal} -> "non-matching case"
  {:exactly, :equal} -> "matching case"
end

We can use pattern matching the same way we already have with the match operator =.

{_mostly, :equal} = {:exactly, :equal}

So now, by matching on :exactly using a variable, we can trigger the first case below.

case {:exactly, :equal} do
  {_mostly, :equal} -> "matching case"
  {:exactly, :equal} -> "exactly equal case"
end

case statements will raise a CaseClauseError if the value provided doesn’t match any case.

case "no match" do
  :sunny -> "t-shirt"
end

To provide a default case for the case statement, you can use a variable, which pattern matches with any Elixir value.

case "no match" do
  "sunnny" -> "wear a t-shirt"
  anything -> "wear clothing"
end

We’re not using this variable, so we should preface it with an _.

case "no match" do
  "sunnny" -> "wear a t-shirt"
  _anything -> "wear clothing"
end

It’s common to simply use an _ rather than naming the variable.

case "no match" do
  "sunnny" -> "wear a t-shirt"
  _ -> "wear clothing"
end

Your Turn

Return each result from 1 to 5 by changing the condition variable below.

condition = Enum.random([[1, 2, 3], [1|[2,3]], {1, 2}, 
  %{key1: :x, key2: "value"}, {1, :tank, %{key: [1, :guppy, :red]}}, :default])


case condition do
  [1, 2, 3] -> 1
  [_head | _tail] -> 2
  {_, _} -> 3
  %{key1: _, key2: "value"} -> 4
  {1, _, %{key: [1, _, _]}} -> 5
  _ -> "default"
end

Cond

cond stands for condition. cond is best used for control flow with many branching paths that does not work well with pattern matching.

For example, if we expand our weather application to handle conditions that depend on a range of temperatures.

  • If the temperature is below 5 degrees: wear a thick coat.
  • If the temperature is 5-10 degrees: wear a coat.
  • If the temperature is 11-15 degrees: wear a light coat.
  • If the temperature is 16-20 degrees: wear a heavy shirt.
  • If the temperature is 21+ degrees: wear a t-shirt.
flowchart
temperature --> t1[0<] --> r1[thick coat]
temperature --> t2[5-10] --> r2[coat]
temperature --> t3[11-15] --> r3[light coat]
temperature --> t4[16-20] --> r4[heavy shirt]
temperature --> t5[21+] --> r5[t-shirt]

Here’s how we can translate this into a program using cond.

# Change Temperature To Change The Condition Triggered.
temperature = 6

cond do
  temperature >= 21 -> "t-shirt"
  temperature >= 16 and temperature <= 20 -> "heavy shirt"
  temperature >= 11 and temperature <= 15 -> "light coat"
  temperature >= 6 and temperature <= 10 -> "coat"
  temperature <= 5 -> "thick coat"
end

cond accepts a truthy value on the left-hand side of the arrow -> and returns the expression on the right-hand side of the arrow for whichever condition returns true first.

This means that order does matter. Here, even though the integer is above 5, it will never trigger the second condition, because the first condition is always true.

value = 10

cond do
  is_integer(value) -> "value is an integer"
  value > 5 -> "value is above 5"
end

Generally, we want more specific conditions to be higher in priority than less specific conditions.

value = 10

cond do
  value > 5 -> "value is above 5"
  is_integer(value) -> "value is an integer"
end

Like case statements, cond will raise an error if we don’t trigger any condition.

cond do
  false -> ""
end

We can provide a default condition using true.

cond do
  false -> ""
  true -> "default condition"
end

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-69 is a C
  • 1-54 is a D

Example solution

We can solve this problem being very explicit about each condition.

grade = 0

cond do
  85 <= grade and grade <= 100 -> "A"
  70 <= grade and grade <= 84 -> "B"
  55 <= grade and grade <= 59 -> "C"
  1 <= grade and grade <= 54 -> "D"
end

Or we can make use of order to make the code more concise. However, this wouldn’t reveal if grade were an invalid value such as -5, 110, or "hello".

cond do
  grade >= 85 -> "A"
  grade >= 70 -> "B"
  grade >= 55 -> "C"
  true -> "D"
end
grade = Enum.random(1..100)

cond do
  grade >= 85 -> "A"
  grade >= 70 -> "B"
  grade >= 55 -> "C"
  grade >= 1 -> "D"
end

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 a variable named tired which is true or false.
  • Create an unless statement. which returns "awake" unless tired.
tired = Enum.random([true, false])

unless tired do
  "awake"
end

Commit Your Progress

DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.

Run git status to ensure there are no undesirable changes. Then run the following in your command line from the curriculum folder to commit your progress.

$ git add .
$ git commit -m "finish Control Flow reading"
$ git push

We’re proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.

We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.

Navigation

Home Report An Issue FunctionsNaming Numbers