Tuples and several return values

In Rust a tuple groups a fixed number of values of different types. It is the lightest way to return two things from a function or to keep a pair together, and unlike a struct it has no names: the fields are numbers from zero. The type is written in parentheses, and the values are read by position.

Tuples are the reason a function can return more than one value without an out parameter or an allocation. The lesson covers the literal, destructuring, the return case, the unit type and the error for an index that the tuple does not have.

A tuple and its fields

The literal (1, "two", 3.5) has the type (i32, &str, f64), and the three values keep their own types. The fields are reached with a dot and a number: item.0, item.1, item.2.

The program prints all three fields on one line. There is no name to get wrong and no order to remember, but a reader has to know what position two means, which is why a struct is better once the meaning matters.

fn main() { let item = (1, "two", 3.5); println!("{} {} {}", item.0, item.1, item.2); }

rustc --edition 2024 literal.rs
./literal

1 two 3.5

The tuple is a value with a size the compiler knows, so it can live on the stack or in a register like any other value. The lesson on structs compares it with the named version.

Destructuring

A let can take the tuple apart in the same line that binds it: let (number, word) = pair; creates two bindings with the two element types. The pattern is exhaustive by construction, so there is no case to handle.

The program destructures a pair and prints both names. The tuple itself is moved into the pattern, and the two bindings are ordinary values afterwards.

fn main() { let pair = (1, "two"); let (number, word) = pair; println!("{number} {word}"); }

rustc --edition 2024 destructure.rs
./destructure

1 two

Destructuring also works on a reference to a tuple, where the bindings borrow the elements instead of copying them. That is how a function that takes &(T, U) reads its two halves without a copy, and the compiler checks the borrows exactly as it does for a struct. The same pattern appears in a for loop over pairs, which the lesson on loops uses with enumerate. A pattern that can fail needs match instead, as the lesson on patterns explains.

Two values from a function

A function that computes two related values returns them as a tuple, and the caller destructures the result. The signature spells the pair out, so a caller knows both types before reading the body.

The program finds the smallest and largest value of a slice in one pass and prints both. The tuple is built at the end of the function, and the two numbers travel back together.

fn min_max(values: &[i32]) -> (i32, i32) { let mut low = values[0]; let mut high = values[0]; for value in values { if *value < low { low = *value; } if *value > high { high = *value; } } (low, high) } fn main() { let (low, high) = min_max(&[4, 9, 1, 7]); println!("{low} {high}"); }

rustc --edition 2024 return_pair.rs
./return_pair

1 9

Returning a tuple allocates nothing: the two integers are returned the way any small value is. For three or more values, or for values whose meaning is not obvious, the lesson on structs recommends a named type instead.

The unit type and nesting

The empty tuple () is the unit type, and it is what a function without a return type returns. A tuple may also contain tuples, which is how a two-dimensional key or a grouped result is built without a struct.

The program prints the unit value and a nested pair. The debug form of the unit is a pair of parentheses, and the nested tuple keeps its own parentheses, which is the whole output.

fn greet() -> () { println!("hello"); } fn main() { let nothing: () = greet(); let nested = ((1, 2), (3, 4)); println!("{:?} {:?}", nothing, nested); }

rustc --edition 2024 unit.rs
./unit

hello
() ((1, 2), (3, 4))

A function whose body ends with a semicolon returns the unit type, which the lesson on functions shows as the classic return-value mistake. The unit value takes no space, so returning it costs nothing.

A field that does not exist

Tuple fields are numbered from zero to one less than the length. Asking for a field past the end is E0609, and the message lists the fields that do exist, so the fix is visible in the error.

The program below reads field three of a pair. The compiler reports the type, the missing field and the available ones, which makes this one of the shortest diagnostics in the language.

fn main() { let pair = (1, 2); println!("{}", pair.3); }

rustc --edition 2024 field_err.rs

error[E0609]: no field `3` on type `({integer}, {integer})`
--> field_err.rs:3:25
|
3 | println!("{}", pair.3);
| ^ unknown field
|
= note: available fields are: `0`, `1`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0609`.

A tuple has no method that appends a field, and its length is part of its type, so the error cannot be fixed by growing the value. The same code is pair.1 for the second value. If a name would make the position obvious, the tuple has outgrown itself and a struct is the answer.

Exercises

  1. Write a function that returns a number and a bool, and destructure the result at the call site.
  2. Build a tuple of three values and print only the middle one. Which field name do you use?
  3. Change the min-max function to return a struct with two named fields. Which lines of the caller have to change?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. How are the fields of a tuple reached?

    Answer

    With a dot and a number, such as item.0

    Tuple fields are numbered from zero and read with the dot syntax.

  2. What types can a tuple hold?

    Answer

    Values of different types

    Each position has its own type, which is what makes a tuple useful for mixed results.

  3. What does let (a, b) = pair; do?

    Answer

    It binds each element to its own name

    The pattern destructures the tuple, and each binding gets the type of its position.

  4. What is the unit type?

    Answer

    The empty tuple, written ()

    The unit type carries no information, and a function without a return type returns it.

  5. What does this program print? fn main() { let item = (1, "two", 3.5); println!("{} {} {}", item.0, item.1, item.2); }

    Answer

    1 two 3.5

    The fields are read in the order the print names them.

  6. What does this program print? fn main() { let pair = (1, "two"); let (number, word) = pair; println!("{number} {word}"); }

    Answer

    1 two

    The pattern gives the integer to number and the string to word.

  7. What does this program print? fn min_max(values: &[i32]) -> (i32, i32) { let mut low = values[0]; let mut high = values[0]; for value in values { if *value < low { low = *value; } if *value > high { high = *value; } } (low, high) } fn main() { let (low, high) = min_max(&[4, 9, 1, 7]); println!("{low} {high}"); }

    Answer

    1 9

    The loop keeps the smallest and largest values, which are one and nine.

  8. What does this program print? fn main() { let nothing = (); let nested = ((1, 2), (3, 4)); println!("{:?} {:?}", nothing, nested); }

    Answer

    () ((1, 2), (3, 4))

    The unit prints as empty parentheses and the nested tuple keeps both pairs.

  9. What is the first line of the compiler output for this program? fn main() { let pair = (1, 2); println!("{}", pair.3); }

    Answer

    error[E0609]: no field 3 on type ({integer}, {integer})

    A pair has fields zero and one, and the error lists the fields that exist.

  10. What does this program print? fn split(value: i32) -> (i32, i32) { (value / 2, value % 2) } fn main() { let (half, rest) = split(7); println!("{half} {rest}"); }

    Answer

    3 1

    Seven divided by two is three with remainder one, and the tuple carries both.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev