1.4 Pattern matching

Our functions still lack an important ability: the ability to make decisions. Computers make decisions using branching logic. For example: If some condition is true, do this, otherwise do something else. Haskell has support for this through pattern matching, which is a way to inspect a piece of data. You can define multiple equations for a single function by matching on different values of the inputs, which we call patterns.

showFuelStatus :: Integer -> String
showFuelStatus 0 = "We're all out of fuel, captain!"
showFuelStatus 1 = "We have one fuel core left."
showFuelStatus 2 = "We have two fuel cores left."

This function showFuelStatus, when given an Integer, will output a String based on the value of that Integer. There are three equations defined, each for a different value of the input. If we add this definition to our Lambdas.hs file, we can load it in ghci and try it out.

> showFuelStatus 0
"We're all out of fuel captain!"

> showFuelStatus 1
"We have one fuel core left."

> showFuelStatus 7
*** Exception: Non-exhaustive patterns in function showFuelStatus

As you can see, it works fine if we apply it to inputs that it anticipates, but since it only knows how to handle inputs of 0, 1, or 2, it breaks if we use any other input value. We can fix this by adding a default equation that matches on anything. All we need to do is use a normal variable, just like before when we defined functions.

showFuelStatus :: Integer -> String
showFuelStatus 0 = "We're all out of fuel, captain!"
showFuelStatus 1 = "We have one fuel core left."
showFuelStatus 2 = "We have two fuel cores left."
showFuelStatus x = "We're all good."

The last equation will now be used if none of the others match the input.

> showFuelStatus 7
"We're all good."

That’s much better. What would happen if we move the last equation before all of the others?

showFuelStatus :: Integer -> String
showFuelStatus x = "We're all good."
showFuelStatus 0 = "We're all out of fuel, captain!"
showFuelStatus 1 = "We have one fuel core left."
showFuelStatus 2 = "We have two fuel cores left."

If we load this into ghci,

> :reload
Lambdas.hs:15:1: warning: [-Woverlapping-patterns]
    Pattern match is redundant
    In an equation for ‘showFuelStatus’: showFuelStatus 0 = ...
   |
15 | showFuelStatus 0 = "We're all out of fuel, captain!"
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

we get a warning about overlapping patterns. Since the first equation has a pattern that matches on anything, the other equations will never even be checked, and the first equation will always be used.

> showFuelStatus 0
"We're all good."

This happens because the equations in a definition are checked from top to bottom, one at a time, and the first one that matches is used. Now, move the catch-all equation back to the bottom, and try applying it to a negative number.

showFuelStatus :: Integer -> String
showFuelStatus 0 = "We're all out of fuel, captain!"
showFuelStatus 1 = "We have one fuel core left."
showFuelStatus 2 = "We have two fuel cores left."
showFuelStatus x = "We're all good."
> showFuelStatus (-1)
"We're all good."

That’s going to be a problem. It’s possible that we could accidentally provide it with a negative number, which will give us the wrong result, meaning your ship could end up stranded in the middle of galactic space without any fuel. To fix this, we can use another feature of Haskell called case expressions, which allow us to match on either a piece of data directly, or on the result of an expression. A case expression has the following form.

case expression of
  pattern1 -> result1
  pattern2 -> result2

Start with the case keyword, followed an expression to match on, then the keyword of. Then on the lines below, list out each equation that you want to use for pattern matching. These equations start with the pattern you want to match on, followed by an arrow ->, and then the result for that equation. Here is an example.

checkUniverse :: String
checkUniverse = case 2 + 2 of
  4 -> "The laws of the universe still hold."
  x -> "The universe is broken!"

We are matching on the result of the expression 2 + 2. This case expression has two equations, the first matching on the number 4, and the second matching on anything, which it assigns to the variable x. Try the following exercise.

Exercise 1

Your starship is equipped with advanced warp drives, but they quickly eat up your fuel and cause you to travel forward in time faster than everything around you (time dilation). Sometimes, it is important for you to avoid these relativistic effects, which are explained by Einstein’s theory of special relativity. You can avoid these effects by staying below 10% the speed of light. The speed of light is approximately 300,000,000 meters per second, which you can represent in Haskell as the floating point number 3.0e8. This is the same as the scientific notation shorthand 3.0×108.

In your Lambdas.hs file, write a function with the following type signature,

relativisticEffects :: Double -> String

that takes the speed of your ship, and outputs whether or not you will experience relativistic effects at that speed.

Solution

We can simply divide the speed of our ship by the speed of light, and check if the result is greater than or equal to 0.10, which is 10%. Then we can pattern match on the result, which will be a value of type Bool.

relativisticEffects speed =
  case (speed / 3.0e8) >= 0.10 of
    True  -> "Things are about to get weird!"
    False -> "All good."

Now we can finally fix our faulty showFuelStatus function using case expressions.

showFuelStatus :: Integer -> String
showFuelStatus 0 = "We're all out of fuel, captain!"
showFuelStatus 1 = "We have one fuel core left."
showFuelStatus 2 = "We have two fuel cores left."
showFuelStatus x = case x > 2 of
  True  -> "We're all good."
  False -> "We have negative fuel, captain!"

If we test it in ghci,

> showFuelStatus 7
"We're all good"

> showFuelStatus (-1)
"We have negative fuel, captain!"

we now get a more helpful output.