Haskell series part 5
This is the fifth article of a series on the functional language Haskell for beginners
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 listlist
: 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
andxs
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 anelse
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.
PS: Part 6 can be found here.
If you have a problem and no one else can help. Maybe you can hire the Kalvad-Team.