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:

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 Float and 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 a contains two values which are respectively the character 'a' and a floating number 6.5
  • When we check the definition of our tuple we see that GHCI infered the type of 6.5 as the typeclass Fractional, more about it can be found in Prelude's documentation, and is represented with the type variable b.

Tuples come with a few helpers:

Prelude> fst t
'a'
Prelude> snd t
6.5

The function 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 = ()

As 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

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 if and else condition.

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
-- Let's write this in tuples.hs
isItTheWeekEndAlready :: Int -> Bool
isItTheWeekEndAlready weekDay = if weekDay == 6 || weekDay == 0
                                then True
                                else False
Example with classic if/else

Pretty standard. Now let's rewrite it with pattern matching:

-- Let's add this also in tuples.hs
isItTheWeekEndAlready2 :: Int -> Bool
isItTheWeekEndAlready2 0 = True
isItTheWeekEndAlready2 6 = True
isItTheWeekEndAlready2 weekDay = False
Example with pattern matching

Ok, so we write a function body for 0and 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:

First line:

  • We are defining a function called trd (for "third").
  • 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 a, b and c.
  • 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 c.

Second line:

  • 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".

Conclusion

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.