Thank you for joining us for the fifth part of our Haskell series, you will find the previous article here where I explain tuples and pattern matching.

In this article we are going to continue a little bit more on pattern matching and then discover guards.

Lists and Pattern Matching

Pattern matching is a great feature in Haskell, on the previous article we applied it on a simple value and on tuples, but this is most of the times used on lists.

Let's try it on something simple, we are going to write a function which determines if a list is empty or not:

-- Let's create a new file pattern.hs and paste this :
isEmpty :: [a] -> Bool
isEmpty [] = True
isEmpty list = False

So we create a function called isEmpty which takes as a parameter a list with a type variable a (meaning our list can contain anything, we do not really care here) and will return a boolean (True = empty).

We define 2 patterns here:

  • []: This represents an empty list
  • list: This is the name of our parameter for [a] from the function definition. We put this one last, otherwise we would always "fall" into this condition.

Let's try it out in GHCI:

Prelude> :l pattern.hs
[1 of 1] Compiling Main             ( pattern.hs, interpreted )
Ok, one module loaded.
*Main> a = []
*Main> b = [1, 2, 3]
*Main> isEmpty a
True
*Main> isEmpty b
False

Everything goes as expected, neat.

If you remember the second article, we were discovering lists for the first time and found out that a helper was available to get the first (head) element of a list. Let's try to rewrite it using pattern matching:

theFirst :: [a] -> a
theFirst (x:_) = x

So we create a function called theFirst which takes as a parameter a list with a type variable a and will return the first element of the same type a.

We define a single pattern here (x:_) which deconstructs the lists into:

  • x as the first element of the list (x and xs are frequently used in list based pattern matching in Haskell even though anything else can be used).
  • _ is the remaining of the list and as we discussed on the previous article an underscore in Haskell is a convention for an unused part of a pattern.

Let's try it out in GHCI:

Prelude> :l pattern.hs
[1 of 1] Compiling Main             ( pattern.hs, interpreted )
Ok, one module loaded.
*Main> b = [1, 2, 3]
*Main> theFirst b
1

Great, we got our first element from the list b. But, what would happen on an empty list ?

*Main> a = []
*Main> theFirst a
*** Exception: pattern.hs:7:1-18: Non-exhaustive patterns in function theFirst

Yep, we forgot to take in account the case where a list would be empty and GHCI throws a pretty explicit exception. Let's try to address that:

-- This is the amended version of theFirst
theFirst :: [a] -> a
theFirst [] = error "Sorry, empty lists are not supported"
theFirst (x:_) = x

We added a new line with a pattern matching an empty array and we used error to throw a runtime error with a message. Let's try it out:

Prelude> :l pattern.hs
[1 of 1] Compiling Main             ( pattern.hs, interpreted )
Ok, one module loaded.
*Main> b = []
*Main> theFirst b
*** Exception: Sorry, empty lists are not supported
CallStack (from HasCallStack):
  error, called at pattern.hs:7:15 in main:Main

The program still crashes but at least, it was our decision. If it makes you curious about what is the actual behavior of head , there it is:

*Main> head b
*** Exception: Prelude.head: empty list

As you can see head throws a runtime error as well.

Guards

The concept of guards is also common in the functional programming world, we talked about it in our Elixir article. The idea with guards is closer to the classic if/else scenarios where we test if a value is True or False, whereas pattern matching deconstructs data and binds it to variables inside the function (Remember theFirst (x:_) = x, automatically the first element was bound to x).

To make it clearer let's try to rewrite our function isItTheWeekEndAlready we wrote in the previous article using guards:

-- Let's paste this in a new file guards.hs
isItTheWeekEndAlready3 :: Int -> Bool
isItTheWeekEndAlready3 day
    | day == 0 || day == 6 = True
    | otherwise = False

So a few things to note:

  • It is really similar to the if/else version we wrote
  • We declare the variable name day on top so we can use it the function body (which we do not do in pattern matching for instance)
  • There is a "catch all" called otherwise (again, like an else in the good old if/else approach)

Let's try to run it:

*Main> :l guards.hs 
[1 of 1] Compiling Main             ( guards.hs, interpreted )
Ok, one module loaded.
*Main> isItTheWeekEndAlready3 1
False
*Main> isItTheWeekEndAlready3 0
True
*Main> isItTheWeekEndAlready3 6
True

Looks good.

There's one more thing which is pretty helpful when using guards called where. It allows us to define a context (using variables) once and use it in the guards. Let's demonstrate it in a function which calculates the ratio of the number of breakfasts you should have eaten in a year (we will assume that everyone understands it is the most important meal of the day). You will get a message based on said ratio:

-- Let's add this to guards.hs
numberOfBreakfasts :: Float -> String
numberOfBreakfasts nb
    | ratio < 0.5 = "Oh no"
    | ratio < 1 = "Not bad"
    | ratio == 1 = "Perfect"
    | otherwise = "Tell me your secret"
    where ratio = nb / 365

As you can see, instead of writing three time this nb / 365 in each guard, we used where to define it once. Let's try it out:

*Main> :l guards.hs 
[1 of 1] Compiling Main             ( guards.hs, interpreted )
Ok, one module loaded.
*Main> numberOfBreakfasts 100
"Oh no"
*Main> numberOfBreakfasts 200
"Not bad"
*Main> numberOfBreakfasts 365
"Perfect"
*Main> numberOfBreakfasts 500
"Tell me your secret"

Looks good, I personally got "Perfect" but I do not want to brag.

Conclusion

Five down, around five more articles to go, we are half way ! We are done with pattern matching and guards this time, I promise. In our next article we are going to discuss about something a bit more complex but essential to understand Haskell: higher order functions.

If you have a problem and no one else can help. Maybe you can hire the Kalvad-Team.