Photo by Md. Zahid Hasan Joy on Unsplash

Tuples

This post presents one more built-in type, the tuple. We then show how vectors, hashmaps, and tuples work together.

One note: there is no consensus on how to pronounce “tuple”. Some people say “tuhple”, which rhymes with “supple”. But in the context of programming, most people say “toople”, which rhymes with “quadruple”.

Tuples are immutable, sort of

A tuple is a sequence of values. The values can be any type, and we index them by integers, so in that respect tuples are a lot like vectors. The important differences are that tuples can contain values of several types. And they are immutable by default. There is a way to mutate the values inside a tuple, by declaring them as mutable references. Use cases for that are rare.

A tuple is a comma-separated vector of values:

let x = ("hello", 42, true);

To create a tuple with a single element, you have to include a final comma:

let y = (true,);

You can even have a tuple with no values at all inside:

let z: () = ();

This is the value that we return from functions that don’t have a type declaration for a return value. And also from expressions that has no value:

let mut a = 0;

let b: () = if true { a+= 1; } ;

A value in parentheses is not a tuple.

Addressing a value inside a tuple looks a little different from how we do it with vectors and slices. There we could use brackets, but for tuples we use dot-notation instead, like so:

fn main() {
    let t = ("hello", 42, true);
    println!("first: {}, second: {} last: {}", t.0, t.1, t.2);
}

This prints out the following:

first: hello, second: 42 last: true

The relational operators work with tuples and other sequences. Rust starts by comparing the first element from each sequence. If they are equal, it goes on to the next elements, and so on, until it finds elements that differ. Following elements are not considered (even if they are big).

Try this, and muck around with the tuples and see for yourself how this plays out:

fn main() {
    let t = ("hello", 42, true);
    let u = ("Hello", 42, true);
    if t < u {
        println!("t is less");
    } else if u < t {
        println!("u is less");
    } else if t == u {
        println!("t and u are equal");
    }
    println!("first: {}, second: {} last: {}", t.0, t.1, t.2);
}

This is all based on the traits that the types of the contained values have. If the types inside all have one or more of the following traits, then the tuple itself also has it:

- Clone
- Copy
- PartialEq
- Eq
- PartialOrd
- Ord
- Debug
- Default
- Hash

There is a quirk in the way Rust implements traits for Tuple. They only work for tuples with 12 values or less at the moment. This may change in the future, but don’t hold your breath. If you need tuples with more than 12 values, you’re better off rethinking your program anyway…

Tuple destructuring

You can divide a tuple into separate variables if you like, like so:

    let a = ("world!", "Hello ");
    let (c, b) = a;
    println!("{}{}", b, c);

This will print out the familiar Hello world!.

You don’t need to extract all values if you don’t want to. Mark the ones you don’t need with an underscore (_) in the left hand parentheses, like so:

    let t = ("hello", 42, true);
    let (a, b, _) = t;
    println!("a: {}, b: {}", a,b);

The printout from this will be: a: hello, b: 42.

Tuples as return values

A function can only return one value, but if the value is a tuple, the effect is the same as returning many values. For example, if you want to divide two integers and compute the quotient and rest. It would be inefficient to compute one first and then the other. It is better to compute them both at the same time.

These use cases are rare. And chances are that you’re doing more in your function than you should, if you think it’s a good idea to return a tuple. And the bigger the tuple you want to return, the more likely it is that you need to rethink your function. It is doing too much.

Every function should have one, and only one, responsibility. That usually means one, and only one return value.

vectors and tuples

There is a built-in function zip() that takes two or more iterators and interleaves them. The name of the function refers to a zipper, which interleaves two rows of teeth. This example zips a string and a vector:

    let my_str = "abc";
    let my_vec = vec![0, 1, 2];
    let zipper = my_str.chars().zip(my_vec);
    println!("{:?}", zipper);

The result is a Zip object that knows how to iterate through the pairs.

Zip { a: Chars(['a', 'b', 'c']), b: IntoIter([0, 1, 2]), index: 0, len: 0 }

The most common use of a Zip is in a for loop:

    for pair in zipper {
        println!("{:?}", pair);
    }

This will print out:

('a', 0)
('b', 1)
('c', 2)

So, we’re getting tuples!

A zip object is a kind of iterator , which is any object that iterates through a sequence. Iterators are a lot like vectors in some ways. But unlike vectors, you can’t use an index to select an element from an iterator.

If you want to use vector operators and methods, you can use collect() to turn your Zip into a vector:

    let zipped_vector: Vec<_> = zipper.collect();
    println!("{:?}", zipped_vector);

The result is a vector of tuples. Each tuple contains a character from the string and matching element from the vector.

[('a', 0), ('b', 1), ('c', 2)]

If the sequences are not the same length, the result has the length of the shorter one.

If you need to traverse the elements of a sequence and their indices, you can use the method enumerate():

    let enumeration = my_str.chars().enumerate();
    println!("{:?}", enumeration);
    for (index, character) in enumeration {
        println!("{}, {}", character, index);
    }

The result from enumerate() is an Enumerate. It can iterate over a sequence of pairs. Each pair contains an index (starting from 0) and an element from the given sequence. The first println! above gives us this:

Enumerate { iter: Chars(['a', 'b', 'c']), count: 0 }

From the loop, the output is

a, 0
b, 1
c, 2

HashMaps and tuples

HashMaps have a method called iter(). It returns an iterator. Calling calling() on that iterator with the return type set to Vec<_> returns a sequence of tuples. Each tuple is a key-value pair. Setting the return type to Vec<_> means “a vector with any kind of type”,

We had this HashMap that maps a few English words to their Spanish translations. We can call iter().collect::<Vec<_>>() on it, and print out the result, like this:

let mut eng2span = HashMap::new();
    eng2span.insert("one", "uno");
    let my_vec = vec![("two", "dos"), ("three", "tres"), ("four", "cuatro")];
    let temp: HashMap<_, _> = my_vec.into_iter().collect();
    eng2span.extend(temp);
    let hash_to_vec = eng2span.iter().collect::<Vec<_>>();
    println!("{:?}", hash_to_vec);

Now we get this on stdout:

[("one", "uno"), ("three", "tres"), ("four", "cuatro"), ("two", "dos")]

Yep. A vector of tuples.

As you should expect from a HashMap, the items are in no particular order.

Going in the other direction, you can use a vector of tuples to initialize a new HashMap:

    let english = vec!["one", "two", "three"];
    let spanish = vec!["uno", "dos", "tres"];
    let english2spanish: HashMap<_, _> = english.iter().zip(spanish.iter()).collect();
    println!("{:?}", english2spanish);

Combining iter with zip yields a concise way to create a HashMap.

It is common to use tuples or vectors as keys in HashMaps. A telephone directory might map from last-name, first-name pairs to telephone numbers.

Sequences of sequences

We have focused on vectors of tuples. But almost all the examples in this post also work with vectors of vectors. Or tuples of tuples, and tuples of vectors. It is sometimes easier to talk about sequences of sequences.

In many contexts, we can use any kind of sequence, strings, vectors or tuples. So how should you choose one over the others?

To start with the obvious, strings are more limited than other sequences. The elements have to be characters. They are also immutable. If you need the ability to change the characters in a string you might want to use a vector of characters instead.

Vectors are more common than tuples, in part because they are easier to make mutable. But there are a few cases where you might prefer tuples:

  1. In some contexts, like a return statement, it is often simpler to create a tuple than a vector. Although, if you need to return more than one type from a single function, you are doing too much in that function.

  2. If you want to use a sequence as a dictionary key, you have to use an immutable type like a tuple or string.

  3. If you are passing a sequence as an argument to a function. Using tuples reduces the potential for unexpected behavior due to aliasing. If you have many arguments to a function, it’s possible you are asking too much of a single function. It could mean that you’re better off splitting that function into several.

Debugging

Vectors, HashMaps and tuples are examples of data structures. In this post we are starting to see compound data structures. Like vectors of tuples, or HashMaps that contain tuples as keys and vectors as values. Compound data structures are useful. But they are prone to shape errors. That is, errors caused when a data structure has the wrong type, size, or structure. For example, if you are expecting a vector with one integer and I give you a plain old integer (not in a vector), it won’t work.

This is yet another reason to use extensive testing when writing your programs.

About the author

For the last three decades, I've worked with a variety of technologies. I'm currently focused on fullstack development. On my day to day job, I'm a senior teacher and course developer at a higher vocational school in Malmoe, Sweden. I'm also an occasional tech speaker and a mentor. Do you want to know more? Visit my website!