Multiway Choices with Guards

Introduction: Handling Multiple Choices

Welcome to the final lesson of our course! In the previous lessons, we built a strong foundation in Haskell by writing functions and making simple two-way decisions. However, in real-world programming, decisions are rarely just black and white.

Often, we need to pick an outcome from many possible options. For example, if you are calculating a student's grade, you need to turn a number into an 'A', 'B', 'C', or 'F'. If you are calculating a Body Mass Index (BMI), you might need to categorize it as "underweight", "normal", "overweight", or "obese".

In this lesson, you will learn how to write clean, multi-branch logic in Haskell without creating messy or confusing code.

The Limits of Nested If Then Else

In our previous lesson, we learned how to use if/then/else expressions to choose between two different results. If we try to use if/then/else for more than two choices, we have to start stacking them inside one another.

For example, to calculate a letter grade based on a score, your code might look something like this:

Haskell
grade :: Int -> Char
grade score = 
  if score >= 90 then 'A'
  else if score >= 80 then 'B'
  else if score >= 70 then 'C'
  else 'F'

While this code works and gets the job done, it is repetitive. As you add more conditions, this nested structure becomes harder to read and maintain. Thankfully, Haskell provides a much more elegant tool for making multiple choices: guards.

Introducing Guards: The Grade Function

Guards allow us to evaluate a series of conditions one by one, moving neatly from top to bottom. Let us rewrite our grade function using guards, starting with just the first condition.

Haskell
grade :: Int -> Char
grade score
  | score >= 90 = 'A'

Notice a few key differences here. First, we do not place an equals sign (=) directly after grade score. Instead, we move to the next line, indent, and type a pipe symbol (|). This symbol represents a "guard." After the guard, we write our condition (score >= 90), followed by an equals sign and the result ('A').

Now, let us add the rest of our conditions.

Haskell
grade :: Int -> Char
grade score
  | score >= 90 = 'A'
  | score >= 80 = 'B'
  | score >= 70 = 'C'

When Haskell runs this function, it checks the guards from top to bottom. If we pass a score of 85, Haskell sees that 85 >= 90 is false, so it skips the first line. It then checks 85 >= 80. This is true, so it immediately returns 'B' and ignores the rest.

Just like if/then/else requires a mandatory else branch, our guard chain needs a fallback in case none of the conditions are true. We do this using otherwise.

Haskell
grade :: Int -> Char
grade score
  | score >= 90 = 'A'
  | score >= 80 = 'B'
  | score >= 70 = 'C'
  | otherwise   = 'F'

Despite how it looks, otherwise is not a special language keyword — it is a predefined name from Haskell's standard library that is simply equal to True. Because it is always True, it acts exactly like a final else: if every earlier guard was false, this catch-all guard always matches. If a student scores a 65, Haskell will check the first three guards, find them all to be false, and then safely fall back to the otherwise line, returning 'F'.

Here is a quick picture of how the guards flow from top to bottom, stopping at the first one that is true:

text
grade 65:
  | score >= 90  -> False, skip
  | score >= 80  -> False, skip
  | score >= 70  -> False, skip
  | otherwise    -> True,  return 'F'
Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal