3.18

Tuple

Just like lists and arrays, tuples are also used to store multiple values. They are created with parentheses and their elements are separated by commas.

> ( 1, 2 )
(1,2)

> ( "Mia", "Vincent" )
("Mia","Vincent")

Note: Elm style guide recommends using a space after ( and a space before ) when creating a tuple.

Unlike lists and arrays, tuples can contain values of different types.

> ( 1, 2.5, "Butch" )
(1,2.5,"Butch")

They can even contain other collections such as lists or arrays or tuples themselves.

> ( [ 1, 2 ], [ "Jules", "Wolf" ] )
([1,2],["Jules","Wolf"])

> import Array

> array = Array.fromList [ 5, 6 ]
Array.fromList [5, 6]

> (array, [ 1, 2 ], ( "Jules", "Wolf" ) )
(Array.fromList [5,6],[1,2],("Jules","Wolf"))

How about lists? Can they contain tuples? The answer is yes.

> [ ( 1, 2 ), ( 3, 4 ), ( 5, 6 ) ]
[(1,2),(3,4),(5,6)]

Remember lists can only contain values of the same kind. In the example above, all three tuples have two numbers in them. What happens if we have tuples of different sizes?

> [ ( 1, 2 ), ( 3, 4, 5 ) ]

-------------------------- TYPE MISMATCH -----------------------------
The 2nd element of this list does not match all the previous elements:

12|   [ ( 1, 2 ), ( 3, 4, 5 ) ]
                  ^^^^^^^^^^^
The 2nd element is a tuple of type:

    ( number, number1, number2 )

But all the previous elements in the list are:

    ( number, number1 )

As Elm points out, ( 1, 2 ) and ( 3, 4, 5 ) are different types of values because their sizes are different. For tuples to be of the same type in Elm, they must contain the same number and type of values. So this won’t work either:

> [ ( 1, 2 ), ( "Sansa", "Ygritte" ) ]  -- This also throws an error

The first tuple is a pair of numbers, whereas the second is a pair of strings which makes them different types. And a list can’t have values of different types.

Length of a Tuple

We can only have up to three elements in a tuple. If we add one more, we’ll get an error. It’s better to use a record when we need to store more than three values of different types.

> ( 1, 2, 3, 4 )   -- This throws an error

Modifying Tuples

In the previous sections, we learned that lists and arrays are immutable. Once they are created, we can never change them. However, despite them being immutable, we were still able to add and remove values from them.

> myList = [ 1, 2, 3, 4 ]
[1,2,3,4]

> myList ++ [ 5 ]
[1,2,3,4,5]

> List.drop 1 myList
[2,3,4]

How’s that possible? It’s possible because on the surface Elm makes it look like it’s modifying an existing list when in fact behind the scenes it creates a completely new list. When we print myList, we discover that its values are intact.

> myList
[1,2,3,4]

From practical standpoint all we care about is the ability to add and remove values from a collection. How Elm enables us to do that is of little concern to us.

Note: Although, in chapter 4 we will find out that immutability does have far-reaching consequences. It’s at the core of some of Elm’s truly exciting features.

Like lists and arrays, tuples are also immutable. But, Elm goes a step further to keep a tuple’s immutability intact. It strips away our ability to add or remove values from a tuple. That’s why there are no functions or operators in the Tuple module to perform those actions.

When you think about it, it does make sense to put this restriction on tuples. Earlier we discovered that for tuples to be of the same type in Elm, they must contain the same number of values. If we were to add or remove a value from a tuple, we would essentially be changing its underlying type as well which is not the case with a list or an array.

Furthermore, if we could add or remove values from tuples, they would start looking a lot like lists and arrays with only one difference: the ability to hold different types of values. In the next section, we will find out that there already exists a data structure called record that can hold different types of values. So without this rigidity, tuples can be easily replaced with other data structures.

Because we can’t add or remove values from tuples, we should only use them when we know in advance how many elements we will need.

Using Tuples

Despite their rigidity, tuples can be quite useful. Listed below are a few scenarios where using tuples makes our lives easier.

Representing Complex Data

Tuples can be used to represent a wide variety of data. For example, if we want to represent someone’s name, age, and a list of siblings we can easily do that with a tuple.

> ( "Jon Snow", 14, [ "Sansa", "Arya", "Bran", "Rob", "Rickon" ] )
("Jon Snow",14,["Sansa","Arya","Bran","Rob","Rickon"])

Returning Multiple Values From a Function

It’s a common pattern in Elm to use tuples for returning multiple values from a function. To see how this works, let’s write a function that determines whether or not a given email is valid. Add the following function definition right above main in Playground.elm.

validateEmail email =
    let
        emailPattern =
            "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b"

        regex =
            Maybe.withDefault Regex.never <|
                Regex.fromString emailPattern

        isValid =
            Regex.contains regex email
    in
    if isValid then
        ( "Valid email", "green" )

    else
        ( "Invalid email", "red" )

Now change the main function to this:

main =
    validateEmail "thedude@rubix.com"
        |> Debug.toString
        |> Html.text

Finally import the Regex module right below the line that imports the Html module.

import Html
import Regex

Run elm reactor from the beginning-elm directory in terminal if it’s not running already and go to http://localhost:8000/src/Playground.elm on your browser. You should see ("Valid email","green").

The validateEmail function uses a regular expression to determine whether a given email is valid or not. It returns a tuple containing two values:

  • If the email is valid, the "Valid email" string is returned, otherwise "Invalid email" is returned.

  • The color to use when displaying the validity status text.

As you can see, using a tuple made it very easy for us to return multiple values.

Being Explicit About the Structure of Data

Let’s say we want to compute the perimeter of a triangle using the formula shown below.

One way to represent the sides of a triangle is by using a list.

> triangleSides = [ 5, 4, 6 ]
[5,4,6]

Now let’s write a function that calculates perimeter in the repl.

> trianglePerimeter sides = List.sum sides
<function>

We used List.sum to add up all sides of a triangle. If we apply this function to the triangleSides list we created above, we will get the perimeter.

> trianglePerimeter triangleSides
15

Now what happens when we apply the trianglePerimeter function to a list that contains only two sides?

> trianglePerimeter [ 5, 4 ]
9

It will happily compute the perimeter. If we want to make sure that the trianglePerimeter always receives exactly three sides, we will have to write some additional code to verify that an input list has only three elements. But if we use a tuple instead, Elm will do that for us. Let’s try it.

> trianglePerimeter ( a, b, c ) = a + b + c
<function>

> trianglePerimeter ( 5, 4, 6 )
15

So far so good. Now let’s apply trianglePerimeter to a tuple with only two elements.

> trianglePerimeter ( 5, 4 )

---------------------- TYPE MISMATCH ------------------------
The 1st argument to `trianglePerimeter` is not what I expect:

6|   trianglePerimeter ( 5, 4 )
                       ^^^^^^^^
This argument is a tuple of type:

    ( number, number1 )

But `trianglePerimeter` needs the 1st argument to be:

    ( number, number, number )

There you go. Elm points out that we need to pass in a tuple with exactly three elements. Picking a right data structure for the right problem often gets rid of subtle issues like this.

Retrieving Values

Elm provides two functions (first and second) for retrieving values from a tuple with two elements also known as a pair. Storing data in pairs is quite common in Elm. first returns the first element in a pair and second returns the second element.

> Tuple.first ( 5, 10 )
5

> Tuple.second ( 5, 10 )
10

> Tuple.first ( "Pam", "Jim" )
"Pam"

> Tuple.second ( "Pam", "Jim" )
"Jim"

Note: elm repl automatically loads the Tuple module. That’s why we don’t need to import it.

There is no function for retrieving the third element in a tuple. But we can use pattern matching for that. You already saw an example of this above. Here it is again.

> trianglePerimeter ( a, b, c ) = a + b + c
<function>

> trianglePerimeter ( 5, 4, 6 )
15

trianglePerimeter uses pattern matching to deconstruct a tuple passed to it and assigns the first, second, and third values to a, b, and c respectively.

Pattern Matching
As mentioned in the Case Expression section earlier in this chapter, pattern matching allows us to check one or more inputs against a pre-defined pattern and see if they match. The pattern we are checking for in the above example is a tuple that contains three, and only three, numbers. We will look into some more examples of pattern matching with tuples in chapter 4.

Mapping Pairs

The Tuple module also provides three functions for transforming values in a pair:

  • mapFirst — maps the first element.
  • mapSecond — maps the second element.
  • mapBoth — maps both elements.
> Tuple.mapFirst (\_ -> "Jim") ( "Roy", "Pam" )
("Jim","Pam")

> Tuple.mapFirst String.reverse ( "live", "life" )
("evil","life")

> Tuple.mapSecond (\x -> x + 1) ( "fringe", 100 )
("fringe",101)

> Tuple.mapSecond (String.contains "-") ( "Gamora", "Star-Lord" )
("Gamora",True)

> Tuple.mapBoth String.length sqrt ( "Newman", 9 )
(6, 3)

There is no function for mapping the third element in a tuple. Tuples really are rigid, aren’t they?

Back to top
Close