Thank you for joining us for the fourth part of our Haskell series, you will find the previous article here where I explain infix and prefix functions, type variables and typeclasses.
In this article we are going to cover tuples and pattern matching. Let's start with the tuples:
For those of you who wrote some python code, tuples must be something you are used to. For the others, think of it as a data structure which allows you to store multiple values into a single variable. Said like this, it sounds a lot like lists which we saw in the second article.
Let's check it out:
Prelude> a = (1, 2) Prelude> b = (1, 2, 3) Prelude> c = [1, 2] Prelude> d = [1, 2, 3]
Well, it all looks very similar, parenthesis instead of square brackets, except that:
- Tuples are immutable, meaning you cannot add more elements as you would with a list. It means that usually, when you use tuples you know exactly how many elements you need.
- Tuples can contain different types of values, for instance
Char, which is not possible with lists.
Let's demonstrate that last point:
Prelude> a = ('a', 6.5) Prelude> a ('a',6.5) Prelude> :t a t :: Fractional b => (Char, b)
So, a few things to unpack here, which luckily we covered in the third article:
- Our tuple
acontains two values which are respectively the character
'a'and a floating number
- When we check the definition of our tuple we see that GHCI infered the type of
6.5as the typeclass
Fractional, more about it can be found in Prelude's documentation, and is represented with the type variable
Tuples come with a few helpers:
Prelude> fst t 'a' Prelude> snd t 6.5
fst (for "first") returns the first element of a tuple and the function
snd (for "second") returns the second element of a tuple. Please note that
snd obviously cannot work if your tuple contains less than 2 values:
Prelude> t = (1) Prelude> snd t <interactive>:18:1: error: • Non type-variable argument in the constraint: Num (a, b) (Use FlexibleContexts to permit this) • When checking the inferred type it :: forall a b. Num (a, b) => b Prelude> t = ()
GHCI tells us,
snd requires a signature of type
(a, b) which proves again the second point, that tuples can contain different types (if needed).
But some of you might wonder:
Q: If there were a tuple containing three values, is there a helper
trd somewhere in the standard library ?
A: No, see the documentation here.
Q: Then, is there a way to access the index of a tuple just like the way we do with lists ?
A: Kind of, but we will need to use a new concept called "pattern matching".
Pattern matching is something very particular to functional programming, we covered it in another article on Elixir among other features if you are curious. The idea is that when writing functions, you are going to define specific patterns for the parameters and then write specific function body for each of them. Said like that, it sounds a lot like a simple
Let's write a very simple example to start with. We are going to write a tiny function which will tell us if a day of the week is the week end or not. The rules are:
- The days of the week go from 0 to 6 included
- 0 is Sunday
- 6 is Saturday
- The week end will be two days: Sunday and Saturday
Pretty standard. Now let's rewrite it with pattern matching:
Ok, so we write a function body for
6 and if they are not matched, we fall through the last pattern which returns
False. Which is why the order you are writing your patterns is important, it will go from the top to the bottom and the convention is usually to have a "catch all" at the end.
Now that we have a basic idea about pattern matching, let's try to write our
trd function. Our function is going to take a single tuple as a parameter and return it's third value.
Let's try it:
-- Let's add this also in tuples.hs trd :: (a, b, c) -> c trd (_, _, v) = v
Alright, let's break this down:
- We are defining a function called
- It takes in parameter a single tuple with 3 values inside (the tuple is delimited by opening and closing parenthesis).
- Because we want to respect the idea of a tuple being able to contain different types, we use 3 type variables, namely
- Because we want this function to return the third value of a tuple, it make sense to define the return type as the type of the third parameter, here
- We are writing a function body for a specific pattern, without a "catch all" condition.
- The underscores in the pattern is some sort of convention in Haskell for patterns that can be ignored as they are irrelevant. We do not actually care about the first and second value of the tuple here.
- Our pattern identify the third parameters using
v(which could be absolutely anything else by the way) and returns it, simple.
Let's try it now:
*Main> :l tuples.hs [1 of 1] Compiling Main ( tuples.hs, interpreted ) Ok, one module loaded. *Main> a = (1, 2, 3) *Main> trd a 3
Looks good. What about running it on a tuple with only 2 values inside:
*Main> b = (1, 2) *Main> trd b <interactive>:26:5: error: • Couldn't match expected type ‘(a0, b0, c)’ with actual type ‘(a1, b1)’ • In the first argument of ‘trd’, namely ‘b’ In the expression: trd b In an equation for ‘it’: it = trd b • Relevant bindings include it :: c (bound at <interactive>:26:1)
Yes, it explodes as we are expecting
(a, b, c).
Please note that this is not a pattern matching problem, this is a function definition problem.We defined the function as
trd :: (a, b, c) -> c explicitly, it cannot be solved by adding a new pattern matching for let's say
trd (_, _) = "oops".
Four down, around six more articles to go. I think this article was slightly easier to understand than the previous one which had pretty unusual topics; especially if you are not coming from a functional programming background. In our next article we are going to continue on the pattern matching line by discussing guards.
If you have a problem and no one else can help. Maybe you can hire the Kalvad-Team.