Skip to content
Learn Rust
Theme

Multiple Return Values

Basics · Lesson 14

To return several values, a Rust function returns one tuple, and the caller unpacks it.

Coming from GoGo returns several values, but Rust returns one tuple, (i32, i32), and unpacks it with let (lo, hi) = .... Because it's one value, you can also keep it whole in a variable and read pair.0 and pair.1, which Go refuses. A Go (value, err) pair is written as a Result in Rust, not as a tuple.

Coming from KotlinA tuple is like Pair or Triple, with as many slots as you write, and it is destructured the same way. The fields are .0 and .1 rather than first and second.

Coming from PythonTuples work much like Python's: (lo, hi) to build, let (lo, hi) = ... to unpack, _ to skip. The type of each slot is fixed, and you read a slot with t.0, not t[0]. A Rust tuple in a mut variable can change in place; a Python tuple cannot.

Coming from TypeScript(i32, i32) is your [number, number] tuple type. A Rust tuple isn't an array, though: you read t.0 and t.1, you can't loop over it, and you skip a slot with _ instead of leaving it empty.

Coming from JavaJava has no built-in tuple type, so you'd write a small record; Rust's (i32, i32) is that record without a name or a declaration. You unpack it with let (lo, hi) = ..., and _ skips a value you don't need.

Coming from C#(i32, i32) is your value tuple (int, int), and let (lo, hi) = ... is deconstruction with _ as the discard. The difference is that Rust tuple fields have no names: you use .0 and .1, or reach for a struct.

Coming from CC has no tuples, so you return a small struct or write through pointers; Rust's (i32, i32) is like an anonymous struct you never have to declare, but its layout is not promised, so it is not a C struct. let (lo, hi) = ... unpacks it, and _ ignores a field.

Coming from C++(i32, i32) is std::pair<int, int> (or std::tuple), and let (lo, hi) = ... is a structured binding. _ skips a slot without naming it, and (a, b) = f(); assigns into existing variables, as std::tie does.

The quick comparison

Rustsrc/main.rsRun ↗
fn min_max(xs: &[i32]) -> (i32, i32) {
let mut lo = xs[0];
let mut hi = xs[0];
for &x in xs {
// each item is &i32; the &x pattern takes the number out
if x < lo {
lo = x;
}
if x > hi {
hi = x;
}
}
(lo, hi)
}
fn main() {
let (lo, hi) = min_max(&[4, 9, 1, 7]);
println!("{lo} {hi}");
let (_, only_hi) = min_max(&[3, 5]); // _ ignores a value
println!("{only_hi}");
}
$ cargo run
1 9
5
package main
import "fmt"
func minMax(xs []int) (int, int) {
lo, hi := xs[0], xs[0]
for _, x := range xs {
if x < lo {
lo = x
}
if x > hi {
hi = x
}
}
return lo, hi
}
func main() {
lo, hi := minMax([]int{4, 9, 1, 7})
fmt.Println(lo, hi)
_, onlyHi := minMax([]int{3, 5})
fmt.Println(onlyHi)
}
$ go run main.go
1 9
5
fun minMax(xs: List<Int>): Pair<Int, Int> {
var lo = xs[0]
var hi = xs[0]
for (x in xs) {
if (x < lo) lo = x
if (x > hi) hi = x
}
return Pair(lo, hi)
}
fun main() {
val (lo, hi) = minMax(listOf(4, 9, 1, 7))
println("$lo $hi")
val (_, onlyHi) = minMax(listOf(3, 5))
println(onlyHi)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
1 9
5
def min_max(xs: list[int]) -> tuple[int, int]:
lo = hi = xs[0]
for x in xs:
if x < lo:
lo = x
if x > hi:
hi = x
return lo, hi
lo, hi = min_max([4, 9, 1, 7])
print(f"{lo} {hi}")
_, only_hi = min_max([3, 5]) # _ ignores a value
print(only_hi)
$ python3 main.py
1 9
5
// a tuple type: an array with a fixed length and a type per slot
function minMax(xs: readonly number[]): [number, number] {
let lo = xs[0];
let hi = xs[0];
for (const x of xs) {
if (x < lo) lo = x;
if (x > hi) hi = x;
}
return [lo, hi];
}
const [lo, hi] = minMax([4, 9, 1, 7]);
console.log(`${lo} ${hi}`);
const [, onlyHi] = minMax([3, 5]); // leave a slot empty to skip it
console.log(onlyHi);
$ tsc --strict --module nodenext --types node main.ts && node main.js
1 9
5
import java.util.List;
public class Main {
// Java has no built-in tuple, so a tiny record holds the two values
record MinMax(int lo, int hi) {}
static MinMax minMax(List<Integer> xs) {
int lo = xs.get(0);
int hi = xs.get(0);
for (int x : xs) {
if (x < lo) lo = x;
if (x > hi) hi = x;
}
return new MinMax(lo, hi);
}
public static void main(String[] args) {
var r = minMax(List.of(4, 9, 1, 7));
System.out.println(r.lo() + " " + r.hi());
int onlyHi = minMax(List.of(3, 5)).hi();
System.out.println(onlyHi);
}
}
$ javac Main.java && java Main
1 9
5
var (lo, hi) = MinMax([4, 9, 1, 7]);
Console.WriteLine($"{lo} {hi}");
var (_, onlyHi) = MinMax([3, 5]); // _ ignores a value
Console.WriteLine(onlyHi);
static (int Lo, int Hi) MinMax(int[] xs)
{
int lo = xs[0], hi = xs[0];
foreach (var x in xs)
{
if (x < lo) lo = x;
if (x > hi) hi = x;
}
return (lo, hi);
}
$ dotnet run
1 9
5
#include <stdio.h>
// C has no tuples: return a small struct instead
struct MinMax {
int lo;
int hi;
};
struct MinMax min_max(const int *xs, size_t len) {
struct MinMax r = {xs[0], xs[0]};
for (size_t i = 0; i < len; i++) {
if (xs[i] < r.lo) r.lo = xs[i];
if (xs[i] > r.hi) r.hi = xs[i];
}
return r;
}
int main(void) {
int a[] = {4, 9, 1, 7};
struct MinMax r = min_max(a, 4);
printf("%d %d\n", r.lo, r.hi);
int b[] = {3, 5};
printf("%d\n", min_max(b, 2).hi); // only use the field you need
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
1 9
5
#include <iostream>
#include <tuple>
#include <utility>
#include <vector>
std::pair<int, int> min_max(const std::vector<int>& xs) {
int lo = xs[0];
int hi = xs[0];
for (int x : xs) {
if (x < lo) lo = x;
if (x > hi) hi = x;
}
return {lo, hi};
}
int main() {
auto [lo, hi] = min_max({4, 9, 1, 7}); // structured binding
std::cout << lo << " " << hi << "\n";
int only_hi;
std::tie(std::ignore, only_hi) = min_max({3, 5}); // std::ignore skips a value
std::cout << only_hi << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
1 9
5

What's different

  • &[i32] is a slice: a borrowed view of some numbers. It accepts a Vec, an array, or part of one. More in the Vectors and Borrowing lessons.
  • Tuples can hold mixed types: let t: (i32, &str) = (1, "a"); then t.0, t.1.
  • Swapping needs no temporary variable: (a, b) = (b, a);.
  • Don't reach for a tuple to return a value plus an error, as in (value, err). Rust uses Result for that (see Errors).
Gotcha

Indexing a tuple like a list

Looks fine: in Python and TypeScript it runs fine. Rust won't compile it.

No direct equivalent in Go. Just a Rust rule.Kotlin refuses this too. Same idea, different words.Works in Python. In Rust it's just a rule.Works in TypeScript. In Rust it's just a rule.No direct equivalent in Java. Just a Rust rule.C# refuses this too. Same idea, different words.No direct equivalent in C. Just a Rust rule.C++ refuses this too. Same idea, different words.
Rustsrc/main.rs✗ won't compile
fn main() {
let t = (3, 4);
println!("{}", t[0]);
}
Gono equivalent

Go has no tuple type. A function returns several values, and you name each one at the call, so there is nothing to index.

Kotlin✗ refuses too

Kotlin refuses too: a Pair has no get operator, so t[0] is an error. You write t.first, or destructure with val (a, b) = t.

fun main() {
val t = Pair(3, 4)
println(t[0])
}
Main.kt:3:14: error: no 'get' operator method providing array access.
    println(t[0])
             ^^^
t = (3, 4)
print(t[0])
$ python3 main.py✓ runs
3
const t: [number, number] = [3, 4];
console.log(t[0]);
$ tsc --strict --module nodenext --types node main.ts && node main.js✓ runs
3
Javano equivalent

Java has no built-in tuple type. A record has accessor methods such as r.lo(), and nothing to index.

C#✗ refuses too

C# refuses too (CS0021): a value tuple cannot be indexed. You write t.Item1, or a name you gave the slot.

(int, int) t = (3, 4);
Console.WriteLine(t[0]);
Program.cs(2,19): error CS0021: Cannot apply indexing with [] to an expression of type '(int, int)'
Cno equivalent

C has no tuple type. A struct has members you reach with a dot, and an array is indexed with brackets.

C++✗ refuses too

C++ refuses too: std::tuple has no operator[]. You write std::get<0>(t), and the index has to be known at compile time (Rust is stricter: a literal).

#include <iostream>
#include <tuple>
int main() {
std::tuple<int, int> t{3, 4};
std::cout << t[0] << "\n";
}
main.cpp: In function ‘int main()’:
main.cpp:6:19: error: no match for ‘operator[]’ (operand types are ‘std::tuple<int, int>’ and ‘int’)
    6 |     std::cout << t[0] << "\n";
      |                   ^
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0608?
error[E0608]: cannot index into a value of type `({integer}, {integer})`
 --> src/main.rs:3:21
  |
3 |     println!("{}", t[0]);
  |                     ^^^ help: to access tuple element `0`, use: `.0`
  |
  = help: tuples are indexed with a dot and a literal index: `tuple.0`, `tuple.1`, etc.
What happens in Go

Go has no tuple type. A function returns several values, and you name each one at the call, so there is nothing to index.

What happens in Kotlin

Kotlin refuses too: a Pair has no get operator, so t[0] is an error. You write t.first, or destructure with val (a, b) = t.

What happens in Python

Python indexes a tuple with brackets, and the index can be a variable. It runs and prints 3.

What happens in TypeScript

A TypeScript tuple is an array with a fixed length and a type per slot, so t[0] is fine and prints 3.

What happens in Java

Java has no built-in tuple type. A record has accessor methods such as r.lo(), and nothing to index.

What happens in C#

C# refuses too (CS0021): a value tuple cannot be indexed. You write t.Item1, or a name you gave the slot.

What happens in C

C has no tuple type. A struct has members you reach with a dot, and an array is indexed with brackets.

What happens in C++

C++ refuses too: std::tuple has no operator[]. You write std::get<0>(t), and the index has to be known at compile time (Rust is stricter: a literal).

What Rust sees

A tuple's slots are fields, numbered from zero, and you reach them with a dot and a literal number: t.0. Square brackets are not defined for a tuple.

Why Rust says no

Each slot can have its own type. A bracket index whose number is only known when the program runs would have to produce a different type for each number, and the compiler has to know the type of every expression before the program runs. t.0 names the slot in the source, so its type is known. The compiler's help: shows the dot form.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
fn main() {
let t = (3, 4);
println!("{}", t.0);
}
$ cargo run
3

The toolkit

A tuple groups a fixed number of values (its slots; the std docs say elements), and each slot can have its own type. Rust has no special syntax for returning several values: a function returns one tuple, and the caller unpacks it. Outputs come from Linux on 64-bit ARM with Rust 1.99.1.

A tuple has numbered slots

The type is written like the value: (i32, &str, bool). You read a slot with a dot and a literal number, t.0, t.1 (Declarations and Types).

Rustsrc/main.rsRun ↗
use std::mem::size_of;
fn main() {
let point = (3, 4);
println!("point.0: {}, point.1: {}", point.0, point.1);
// Each slot has its own type.
let record: (i32, &str, bool) = (7, "seven", true);
println!("record: {record:?}");
println!("record.1: {}", record.1);
// Tuples nest, and each .N goes one level in.
let nested = ((1, 2), "x");
println!("nested.0.1: {}", nested.0.1);
// A tuple changes in place only if the variable is `mut`.
let mut counter = (0, "hits");
counter.0 += 10;
println!("counter: {counter:?}");
// size_of::<T>() is the size of T in bytes.
println!("size_of::<(i32, i32)>(): {}", size_of::<(i32, i32)>());
println!("size_of::<(u8, u32)>(): {}", size_of::<(u8, u32)>());
}
$ cargo run
point.0: 3, point.1: 4
record: (7, "seven", true)
record.1: seven
nested.0.1: 2
counter: (10, "hits")
size_of::<(i32, i32)>(): 8
size_of::<(u8, u32)>():  8

The slots sit inside the variable: (i32, i32) is 8 bytes, just the two numbers. (u8, u32) is 8 too, not 5, because the compiler adds padding for alignment and the language lets it reorder slots. The layout of a tuple is not promised; the sizes are what this compiler printed.

A tuple is not a list. t[0] is refused because the slot has to be a literal, so the compiler knows its type. A for loop is refused because the slots can have different types, so there is no single type for x:

Rustsrc/main.rs✗ won't compile
fn main() {
let t = (3, 4);
println!("first: {}", t[0]);
}
$ cargo buildWhat is E0608?
error[E0608]: cannot index into a value of type `({integer}, {integer})`
 --> src/main.rs:3:28
  |
3 |     println!("first: {}", t[0]);
  |                            ^^^ help: to access tuple element `0`, use: `.0`
  |
  = help: tuples are indexed with a dot and a literal index: `tuple.0`, `tuple.1`, etc.
Rustsrc/main.rs✗ won't compile
fn main() {
let t = (3, 4, 5);
for x in t {
println!("{x}");
}
}
$ cargo buildWhat is E0277?
error[E0277]: `({integer}, {integer}, {integer})` is not an iterator
 --> src/main.rs:3:14
  |
3 |     for x in t {
  |              ^ `({integer}, {integer}, {integer})` is not an iterator
  |
  = help: the trait `Iterator` is not implemented for `({integer}, {integer}, {integer})`
  = note: required for `({integer}, {integer}, {integer})` to implement `IntoIterator`
Tuple (A, B) Array [T; N] Vec<T>
Types of the slots each its own all the same all the same
Length fixed, part of the type fixed, part of the type grows
Read a slot t.0, a literal only a[i], any usize v[i]
Loop over it no yes yes

Arrays and Vec are in Vectors. The rows are run by check-rows.rs.

Returning several values

Declare the return type as a tuple and build it as the tail expression (the last line, with no ;; Statements and Expressions). The caller either unpacks the pair into names or keeps it whole:

Rustsrc/main.rsRun ↗
fn divmod(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
fn main() {
// Unpack into two names.
let (q, r) = divmod(17, 5);
println!("q: {q}, r: {r}");
// Or keep the pair whole and read its slots.
let pair = divmod(17, 5);
println!("pair: {pair:?}, pair.0: {}", pair.0);
// `_` ignores a slot.
let (_, rem) = divmod(40, 7);
println!("rem: {rem}");
}
$ cargo run
q: 3, r: 2
pair: (3, 2), pair.0: 3
rem: 5

Unpacking

let (q, r) = ... is a pattern (patterns in let): it has the shape of the value and names its parts (Python calls this unpacking; Kotlin, C# and JavaScript, destructuring). The same shape works in a function’s parameters and in a for loop, and .. skips every slot you do not name. Without .., both sides must have the same number of slots:

Rustsrc/main.rsRun ↗
// A pattern can sit in a parameter list.
fn manhattan((x1, y1): (i32, i32), (x2, y2): (i32, i32)) -> i32 {
(x1 - x2).abs() + (y1 - y2).abs()
}
fn main() {
println!("manhattan: {}", manhattan((1, 2), (4, -2)));
// Nested tuples unpack in one step.
let ((a, b), label) = ((1, 2), "pair");
println!("a: {a}, b: {b}, label: {label}");
// `..` skips everything else.
let four = (10, 20, 30, 40);
let (first, ..) = four;
let (.., last) = four;
println!("first: {first}, last: {last}");
// A `for` loop takes a pattern too.
for (name, score) in [("ana", 3), ("bo", 5)] {
println!("{name}: {score}");
}
}
$ cargo run
manhattan: 7
a: 1, b: 2, label: pair
first: 10, last: 40
ana: 3
bo: 5
Rustsrc/main.rs✗ won't compile
fn main() {
let (a, b, c) = (1, 2);
println!("{a} {b} {c}");
}
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (a, b, c) = (1, 2);
  |         ^^^^^^^^^   ------ this expression has type `({integer}, {integer})`
  |         |
  |         expected a tuple with 2 elements, found one with 3 elements
  |
  = note: expected tuple `({integer}, {integer})`
             found tuple `(_, _, _)`

Python raises an error for the same mistake only when the line runs. Rust refuses it while compiling.

Swapping

The right side of (a, b) = (b, a) is built before anything is assigned, so no temporary is needed. This is destructuring assignment: unlike let, it updates variables that already exist, so a variable that already has a value must be mut:

Rustsrc/main.rsRun ↗
fn main() {
let mut a = 1;
let mut b = 2;
(a, b) = (b, a);
println!("after swap: a: {a}, b: {b}");
// Every new value is computed from the old ones, so rotating three works the same way.
let (mut x, mut y, mut z) = (1, 2, 3);
(x, y, z) = (y, z, x);
println!("rotated: x: {x}, y: {y}, z: {z}");
}
$ cargo run
after swap: a: 2, b: 1
rotated: x: 2, y: 3, z: 1

Tuples the standard library hands you

Many std methods return tuples, and you meet them with the same let (a, b) = .... overflowing_add is used in Types:

Rustsrc/main.rsRun ↗
fn main() {
// split_at: two slices from one.
let (left, right) = [1, 2, 3, 4].split_at(1);
println!("split_at(1): {left:?} and {right:?}");
// overflowing_add: the wrapped sum, and whether it overflowed.
let (sum, overflowed) = 250u8.overflowing_add(10);
println!("250u8.overflowing_add(10): sum: {sum}, overflowed: {overflowed}");
// Both are plain tuples: keep one whole if you like.
let result = 250u8.overflowing_add(10);
println!("result.0: {}, result.1: {}", result.0, result.1);
}
$ cargo run
split_at(1): [1] and [2, 3, 4]
250u8.overflowing_add(10): sum: 4, overflowed: true
result.0: 4, result.1: true

A missing pair, a failed pair

A pair that might not exist is an Option<(A, B)>, and one that might fail is a Result<(A, B), E>: the tuple goes inside. str::split_once returns the first kind. Option and Errors teach how to open them. The type records that the value may be missing, so you cannot reach it without deciding what to do about None or Err. That is why a Go (value, err) pair is a Result in Rust, not a tuple of two values.

Comparing tuples

Tuples compare slot by slot, left to right: the first slot that differs decides, and later slots only break ties (the std docs say this for PartialOrd and Ord). == compares slot by slot too. That makes a list of tuples sortable, and a tuple a usable map key when its parts are (check-rows.rs runs one):

Rustsrc/main.rsRun ↗
fn main() {
println!("(1, 9) < (2, 0): {}", (1, 9) < (2, 0));
println!("(2, 1) < (2, 3): {}", (2, 1) < (2, 3));
println!("(2, 3) < (2, 3): {}", (2, 3) < (2, 3));
println!("(1, 2) == (1, 2): {}", (1, 2) == (1, 2));
println!("(\"b\", 1) > (\"a\", 9): {}", ("b", 1) > ("a", 9));
// Because they compare, a list of tuples sorts: by the first slot, then the second.
let mut results = [(2, "bo"), (1, "zed"), (2, "ana")];
results.sort();
println!("sorted: {results:?}");
}
$ cargo run
(1, 9) < (2, 0): true
(2, 1) < (2, 3): true
(2, 3) < (2, 3): false
(1, 2) == (1, 2): true
("b", 1) > ("a", 9): true
sorted: [(1, "zed"), (2, "ana"), (2, "bo")]

Tuple or struct?

A tuple does not say what its slots mean. When both slots have the same type, the compiler cannot catch a swapped pair:

Rustsrc/main.rsRun ↗
// With a tuple, nothing says which slot is which.
fn aspect(size: (u32, u32)) -> u32 {
size.0 / size.1
}
// A struct names them (the Structs lesson covers this).
struct Size {
width: u32,
height: u32,
}
fn aspect_named(size: Size) -> u32 {
size.width / size.height
}
fn main() {
// Width first, height second? The compiler cannot tell this is swapped.
println!("aspect((16, 9)): {}", aspect((16, 9)));
println!("aspect((9, 16)): {}", aspect((9, 16)));
let size = Size {
width: 16,
height: 9,
};
println!("aspect_named: {}", aspect_named(size));
}
$ cargo run
aspect((16, 9)): 1
aspect((9, 16)): 0
aspect_named: 1

aspect((9, 16)) compiles and gives a different answer. A struct names the fields, so a caller writes width: 16 and cannot mix them up.

Reach for a tuple when Reach for a struct when
the result is short-lived and the caller unpacks it at once the value is stored, passed around or returned from a public API
the slots have different types, so mixing them up does not compile two slots share a type, or the slots keep growing in number

Your language, in Rust

Going deeper

The empty tuple and the one-slot tuple

() is the tuple with no slots, called unit (the value for “nothing”); Result<(), E> means “it worked, there is nothing to hand back”. A one-slot tuple needs a comma, because (5) is just 5 in parentheses:

Rustsrc/main.rsRun ↗
use std::mem::size_of;
fn log(msg: &str) {
println!("log: {msg}");
}
fn check(ok: bool) -> Result<(), String> {
if ok {
Ok(())
} else {
Err("not ok".to_string())
}
}
fn main() {
// A function with no `->` returns the empty tuple.
let nothing = log("hello");
println!("nothing: {nothing:?}");
println!("size_of::<()>(): {}", size_of::<()>());
// `Result<(), E>`: "it worked, there is nothing to return".
println!("check(true): {:?}", check(true));
println!("check(false): {:?}", check(false));
// A one-slot tuple needs the comma.
let one = (5,);
println!("one: {one:?}, one.0: {}", one.0);
}
$ cargo run
log: hello
nothing: ()
size_of::<()>(): 0
check(true): Ok(())
check(false): Err("not ok")
one: (5,), one.0: 5
Rustsrc/main.rs✗ won't compile
fn main() {
let t: (i32,) = (5);
println!("{}", t.0);
}
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:2:21
  |
2 |     let t: (i32,) = (5);
  |            ------   ^^^ expected `(i32,)`, found integer
  |            |
  |            expected due to this
  |
  = note: expected tuple `(i32,)`
              found type `{integer}`
help: use a trailing comma to create a tuple with one element
  |
2 |     let t: (i32,) = (5,);
  |                       +

Tuples stop at twelve, for the standard traits

Any length is a legal tuple type. But the traits that give a tuple {:?} printing, ==, ordering, a default value and hashing (Debug, PartialEq, PartialOrd, Ord, Eq, Default, Hash; see Traits) are implemented only up to 12 slots. The std docs call it a “temporary restriction in Rust’s type system” that “may change”. Clone and Copy have no limit:

Rustsrc/main.rsRun ↗
fn main() {
let twelve = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
println!("twelve: {twelve:?}");
// A 13-slot tuple is a fine type: you can build it, read a slot and copy it...
let thirteen = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
let copy = thirteen;
println!("thirteen.12: {}, copy.12: {}", thirteen.12, copy.12);
// ...but `{:?}` on it is refused (see below).
}
$ cargo run
twelve: (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
thirteen.12: 13, copy.12: 13
Rustsrc/main.rs✗ won't compile
fn main() {
let thirteen = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
println!("thirteen.12: {}", thirteen.12);
println!("thirteen: {thirteen:?}");
}
$ cargo buildWhat is E0277?
error[E0277]: `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})` doesn't implement `Debug`
 --> src/main.rs:4:25
  |
4 |     println!("thirteen: {thirteen:?}");
  |                         ^^^^^^^^^^^^ `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})` cannot be formatted using `{:?}` because it doesn't implement `Debug`
  |
  = help: the trait `Debug` is not implemented for `({integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer}, {integer})`
  = help: the following other types implement trait `Debug`:
            (T₁, T₂, …, Tₙ) up to tuples of arity 12
          and 5 others

Practice

Try it

Open each exercise in the Playground, replace the todo!(), and run it until it prints all checks passed.

Two Fibonacci numbers at once

Return (F(n), F(n + 1)) as a (u64, u64). Keep the pair in two mut variables and update both at once with a destructuring assignment. The checks go as far as F(93), the largest Fibonacci number a u64 holds.

Starterexercises/fib-pair.rs
/// Returns (F(n), F(n + 1)), where F(0) = 0 and F(1) = 1.
fn fib_pair(n: u32) -> (u64, u64) {
todo!("start from (0, 1); update both numbers at once, n times")
}
fn main() {
assert_eq!(fib_pair(0), (0, 1));
assert_eq!(fib_pair(1), (1, 1));
assert_eq!(fib_pair(10), (55, 89));
// F(93) is the largest Fibonacci number that fits in a u64.
assert_eq!(fib_pair(92), (7540113804746346429, 12200160415121876738));
// The caller unpacks the pair.
let (this, next) = fib_pair(20);
assert_eq!(next - this, 4181);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/fib-pair.solution.rsRun ↗
/// Returns (F(n), F(n + 1)), where F(0) = 0 and F(1) = 1.
fn fib_pair(n: u32) -> (u64, u64) {
let (mut a, mut b) = (0, 1);
for _ in 0..n {
(a, b) = (b, a + b);
}
(a, b)
}
fn main() {
assert_eq!(fib_pair(0), (0, 1));
assert_eq!(fib_pair(1), (1, 1));
assert_eq!(fib_pair(10), (55, 89));
// F(93) is the largest Fibonacci number that fits in a u64.
assert_eq!(fib_pair(92), (7540113804746346429, 12200160415121876738));
// The caller unpacks the pair.
let (this, next) = fib_pair(20);
assert_eq!(next - this, 4181);
println!("all checks passed");
}
$ cargo run
all checks passed

The smallest rectangle around some points

Return the bottom-left and top-right corners of a slice of (i32, i32) points, as a tuple of tuples. A check puts the smallest x and the smallest y on different points, and the caller unpacks the result with a nested pattern.

Starterexercises/bounding-box.rs
/// The smallest rectangle that holds every point, as (bottom_left, top_right).
/// `points` is never empty.
fn bounding_box(points: &[(i32, i32)]) -> ((i32, i32), (i32, i32)) {
todo!("start both corners at points[0]; `for &(x, y) in points` unpacks each point")
}
fn main() {
assert_eq!(bounding_box(&[(1, 2)]), ((1, 2), (1, 2)));
assert_eq!(bounding_box(&[(4, 4), (4, 4)]), ((4, 4), (4, 4)));
// Points on a line.
assert_eq!(bounding_box(&[(0, 5), (3, 5)]), ((0, 5), (3, 5)));
// The smallest x and the smallest y come from different points.
assert_eq!(
bounding_box(&[(3, -1), (-2, 5), (0, 0)]),
((-2, -1), (3, 5))
);
// The caller unpacks a tuple of tuples in one step.
let ((x0, y0), (x1, y1)) = bounding_box(&[(0, 0), (6, 4)]);
assert_eq!((x1 - x0) * (y1 - y0), 24);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/bounding-box.solution.rsRun ↗
/// The smallest rectangle that holds every point, as (bottom_left, top_right).
/// `points` is never empty.
fn bounding_box(points: &[(i32, i32)]) -> ((i32, i32), (i32, i32)) {
let (mut x0, mut y0) = points[0];
let (mut x1, mut y1) = points[0];
for &(x, y) in points {
if x < x0 {
x0 = x;
}
if y < y0 {
y0 = y;
}
if x > x1 {
x1 = x;
}
if y > y1 {
y1 = y;
}
}
((x0, y0), (x1, y1))
}
fn main() {
assert_eq!(bounding_box(&[(1, 2)]), ((1, 2), (1, 2)));
assert_eq!(bounding_box(&[(4, 4), (4, 4)]), ((4, 4), (4, 4)));
// Points on a line.
assert_eq!(bounding_box(&[(0, 5), (3, 5)]), ((0, 5), (3, 5)));
// The smallest x and the smallest y come from different points.
assert_eq!(
bounding_box(&[(3, -1), (-2, 5), (0, 0)]),
((-2, -1), (3, 5))
);
// The caller unpacks a tuple of tuples in one step.
let ((x0, y0), (x1, y1)) = bounding_box(&[(0, 0), (6, 4)]);
assert_eq!((x1 - x0) * (y1 - y0), 24);
println!("all checks passed");
}
$ cargo run
all checks passed

Read more