Skip to content
Learn Rust
Theme

Patterns

Data · Lesson 28

A pattern is a shape that a value is taken apart by (destructuring), with a name wherever you want to keep a part. match is where you meet them, but they also stand in let, for, function parameters and if let. The compiler checks that a pattern which must always fit does, and what a pattern does to ownership is worth knowing.

Coming from GoGo has no way to take a struct or an array apart: you read p.x and a[0], and the nearest things are a, b := f(), for i, v := range and v, ok := m[k]. A Rust pattern does those jobs and also takes structs, tuples and enums apart, in let, for, parameters and if let.

Coming from KotlinThis is the idea behind val (a, b) = pair and for ((k, v) in map), with three differences: a struct pattern matches fields by name where Kotlin goes by position (by default), patterns nest, and a pattern can fail (if let), where a destructuring declaration never tests the shape (on a List it calls component2() and can throw).

Coming from PythonThis is tuple unpacking (a, b = pair, first, *rest = xs, for k, v in d.items()), extended to structs and enums. A pattern can also fail on purpose (if let), where unpacking the wrong number of items raises ValueError at run time.

Coming from TypeScriptThis is destructuring (const { x, y } = p, const [a, b] = pair), but a Rust pattern also tests the shape (Some(n)), and the compiler checks the length of an array and the variants instead of leaving a missing item undefined. A pattern has no default values and no object rest, and matching moves where destructuring only adds names.

Coming from JavaJava 21's record patterns (p instanceof Point(int x, int y)) are the nearest thing, and they only appear in instanceof and switch. Rust's work on structs, tuples, arrays and enums, and also stand in let, for and function parameters.

Coming from C#C# deconstruction (var (x, y) = p) and its positional, property and list patterns are the nearest thing. Rust patterns also stand in function and closure parameters and in let ... else, and have | and ranges but no and or not.

Coming from CC has no destructuring: you read p.x and a[0] one at a time (int x = p.x, y = p.y;). A Rust pattern takes a value apart in one step, and the compiler checks that the places where it cannot fail really cannot.

Coming from C++C++17 structured bindings (auto [x, y] = p;) are the nearest thing: by position, no nesting, and no way to test a shape. Rust's patterns nest, bind struct fields by name, and if let tests the shape and takes it apart in one go.

The quick comparison

Rustsrc/main.rsRun ↗
struct Point {
x: i32,
y: i32,
}
fn main() {
// let takes a value apart, by field name
let Point { x, y } = Point { x: 3, y: -2 };
println!("x = {x}, y = {y}");
// so does a for loop, one item at a time
for (i, c) in "abc".chars().enumerate() {
println!("{i}: {c}");
}
// if let takes a value apart only when it fits
if let Some((key, value)) = "mode=fast".split_once('=') {
println!("{key} -> {value}");
}
// [first, .., last] names the ends of an array
let [first, .., last] = [10, 20, 30, 40];
println!("first = {first}, last = {last}");
}
$ cargo run
x = 3, y = -2
0: a
1: b
2: c
mode -> fast
first = 10, last = 40
package main
import (
"fmt"
"strings"
)
type Point struct{ x, y int }
func main() {
// no destructuring of a struct: read the fields
p := Point{3, -2}
x, y := p.x, p.y
fmt.Printf("x = %d, y = %d\n", x, y)
// range gives (index, rune) pairs
for i, c := range "abc" {
fmt.Printf("%d: %c\n", i, c)
}
// strings.Cut returns a found flag
if key, value, found := strings.Cut("mode=fast", "="); found {
fmt.Printf("%s -> %s\n", key, value)
}
// an array is read by index
a := [4]int{10, 20, 30, 40}
first, last := a[0], a[len(a)-1]
fmt.Printf("first = %d, last = %d\n", first, last)
}
$ go run main.go
x = 3, y = -2
0: a
1: b
2: c
mode -> fast
first = 10, last = 40
data class Point(val x: Int, val y: Int)
fun main() {
// destructuring declaration: by position, through component1() and component2()
val (x, y) = Point(3, -2)
println("x = $x, y = $y")
for ((i, c) in "abc".withIndex()) {
println("$i: $c")
}
val kv = "mode=fast".split("=", limit = 2)
if (kv.size == 2) {
val (key, value) = kv
println("$key -> $value")
}
// a List destructures by position too; _ skips an item
val (first, _, _, last) = listOf(10, 20, 30, 40)
println("first = $first, last = $last")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
x = 3, y = -2
0: a
1: b
2: c
mode -> fast
first = 10, last = 40
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
# unpacking is by position
x, y = Point(3, -2)
print(f"x = {x}, y = {y}")
for i, c in enumerate("abc"):
print(f"{i}: {c}")
key, sep, value = "mode=fast".partition("=")
if sep:
print(f"{key} -> {value}")
# * collects the items in the middle
first, *_, last = [10, 20, 30, 40]
print(f"first = {first}, last = {last}")
$ python3 main.py
x = 3, y = -2
0: a
1: b
2: c
mode -> fast
first = 10, last = 40
const { x, y } = { x: 3, y: -2 };
console.log(`x = ${x}, y = ${y}`);
for (const [i, c] of [..."abc"].entries()) {
console.log(`${i}: ${c}`);
}
const kv = "mode=fast".split("=");
if (kv.length === 2) {
const [key, value] = kv;
console.log(`${key} -> ${value}`);
}
// an empty slot skips an item
const [first, , , last] = [10, 20, 30, 40];
console.log(`first = ${first}, last = ${last}`);
$ tsc --strict --module nodenext --types node main.ts && node main.js
x = 3, y = -2
0: a
1: b
2: c
mode -> fast
first = 10, last = 40
public class Main {
record Point(int x, int y) {}
public static void main(String[] args) {
// a record pattern takes the record apart
Point p = new Point(3, -2);
if (p instanceof Point(int x, int y)) {
System.out.println("x = " + x + ", y = " + y);
}
String abc = "abc";
for (int i = 0; i < abc.length(); i++) {
System.out.println(i + ": " + abc.charAt(i));
}
String[] kv = "mode=fast".split("=", 2);
if (kv.length == 2) {
System.out.println(kv[0] + " -> " + kv[1]);
}
// an array is read by index
int[] a = {10, 20, 30, 40};
System.out.println("first = " + a[0] + ", last = " + a[a.length - 1]);
}
}
$ javac Main.java && java Main
x = 3, y = -2
0: a
1: b
2: c
mode -> fast
first = 10, last = 40
using System;
using System.Linq;
var (x, y) = new Point(3, -2);
Console.WriteLine($"x = {x}, y = {y}");
foreach (var (c, i) in "abc".Select((c, i) => (c, i)))
{
Console.WriteLine($"{i}: {c}");
}
if ("mode=fast".Split('=', 2) is [var key, var value])
{
Console.WriteLine($"{key} -> {value}");
}
// a list pattern: .. stands for the items in the middle
int[] a = { 10, 20, 30, 40 };
if (a is [var first, .., var last])
{
Console.WriteLine($"first = {first}, last = {last}");
}
record Point(int X, int Y);
$ dotnet run
x = 3, y = -2
0: a
1: b
2: c
mode -> fast
first = 10, last = 40
#include <stdio.h>
#include <string.h>
struct Point {
int x, y;
};
int main(void) {
// no destructuring: read the fields
struct Point p = {3, -2};
int x = p.x, y = p.y;
printf("x = %d, y = %d\n", x, y);
const char *abc = "abc";
for (int i = 0; abc[i] != '\0'; i++) {
printf("%d: %c\n", i, abc[i]);
}
// strchr returns a pointer to the '=' or NULL
const char *line = "mode=fast";
const char *eq = strchr(line, '=');
if (eq != NULL) {
printf("%.*s -> %s\n", (int)(eq - line), line, eq + 1);
}
int a[] = {10, 20, 30, 40};
printf("first = %d, last = %d\n", a[0], a[3]);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
x = 3, y = -2
0: a
1: b
2: c
mode -> fast
first = 10, last = 40
#include <array>
#include <iostream>
#include <string>
struct Point {
int x, y;
};
int main() {
// structured binding: by position
auto [x, y] = Point{3, -2};
std::cout << "x = " << x << ", y = " << y << "\n";
std::string abc = "abc";
for (size_t i = 0; i < abc.size(); i++) {
std::cout << i << ": " << abc[i] << "\n";
}
std::string line = "mode=fast";
if (auto eq = line.find('='); eq != std::string::npos) {
std::cout << line.substr(0, eq) << " -> " << line.substr(eq + 1) << "\n";
}
// an array is read by index
std::array<int, 4> a = {10, 20, 30, 40};
std::cout << "first = " << a.front() << ", last = " << a.back() << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
x = 3, y = -2
0: a
1: b
2: c
mode -> fast
first = 10, last = 40

What's different

  • A pattern is a shape with names in it. When the value fits, each name holds its part.
  • Patterns stand wherever Rust introduces a variable: let, parameters, for, if let, while let, let ... else, matches! and match.
  • let, parameters and for need a pattern that always fits. A pattern that can fail belongs in if let, let ... else or match.
  • Matching a value moves a part that is not Copy, where destructuring in Python or Kotlin only adds names. Match a reference (&opt, opt.as_ref()) to borrow instead.
  • _ ignores a part and does not move it. _name is a variable that does.
Gotcha

Taking something apart that might not be there

Looks fine: in Go, TypeScript and C++ it runs but gives a wrong answer; in Kotlin, Python, Java, C# and C it crashes at runtime. Rust won't compile it.

Broken in Go. Rust catches it.Broken in Kotlin. Rust catches it.Broken in Python. Rust catches it.Broken in TypeScript. Rust catches it.Broken in Java. Rust catches it.Broken in C#. Rust catches it.Broken in C. Rust catches it.Broken in C++. Rust catches it.
Rustsrc/main.rs✗ won't compile
fn main() {
let line = "mode";
let Some((key, value)) = line.split_once('=');
println!("{key} -> {value}");
}
package main
import (
"fmt"
"strings"
)
func main() {
line := "mode"
key, value, _ := strings.Cut(line, "=")
fmt.Printf("%s -> %s\n", key, value)
}
$ go run main.go! runs · wrong answer
mode -> 
fun main() {
val line = "mode"
val (key, value) = line.split("=")
println("$key -> $value")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar! crashes at runtime
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
	at java.base/java.util.Collections$SingletonList.get(Collections.java:5290)
	at MainKt.main(Main.kt:3)
	at MainKt.main(Main.kt)
line = "mode"
key, value = line.split("=")
print(f"{key} -> {value}")
$ python3 main.py! crashes at runtime
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    key, value = line.split("=")
    ^^^^^^^^^^
ValueError: not enough values to unpack (expected 2, got 1)
const line = "mode";
const [key, value] = line.split("=");
console.log(`${key} -> ${value}`);
$ tsc --strict --module nodenext --types node main.ts && node main.js! runs · wrong answer
mode -> undefined
public class Main {
public static void main(String[] args) {
String line = "mode";
String[] parts = line.split("=");
System.out.println(parts[0] + " -> " + parts[1]);
}
}
$ javac Main.java && java Main! crashes at runtime
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
	at Main.main(Main.java:5)
using System;
var line = "mode";
var parts = line.Split('=');
Console.WriteLine($"{parts[0]} -> {parts[1]}");
$ dotnet run! crashes at runtime
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Program.<Main>$(String[] args) in Program.cs:line 5
#include <stdio.h>
#include <string.h>
int main(void) {
const char *line = "mode";
const char *eq = strchr(line, '=');
printf("%.*s -> %s\n", (int)(eq - line), line, eq + 1);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main! crashes at runtime
#include <iostream>
#include <string>
int main() {
std::string line = "mode";
auto eq = line.find('=');
std::cout << line.substr(0, eq) << " -> " << line.substr(eq + 1) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main! runs · wrong answer
mode -> mode
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0005?
error[E0005]: refutable pattern in local binding
 --> src/main.rs:3:9
  |
3 |     let Some((key, value)) = line.split_once('=');
  |         ^^^^^^^^^^^^^^^^^^ pattern `None` not covered
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
  = note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
  = note: the matched value is of type `Option<(&str, &str)>`
help: you might want to use `let...else` to handle the variant that isn't matched
  |
3 |     let Some((key, value)) = line.split_once('=') else { todo!() };
  |                                                   ++++++++++++++++
What happens in Go

strings.Cut returns a found flag, but key, value, _ := throws it away; the layer-1 program keeps it with if ...; found. go vet accepts this version. For mode it prints mode -> with an empty value and no error.

What happens in Kotlin

val (key, value) = line.split("=") calls component2() on a list with one item, and throws IndexOutOfBoundsException.

What happens in Python

key, value = line.split("=") raises ValueError: not enough values to unpack. Unpacking checks the count, but at run time.

What happens in TypeScript

TypeScript accepts this under --strict: value has type string, and at run time it is undefined, so the program prints mode -> undefined. The option --noUncheckedIndexedAccess, which --strict does not include, types value as string | undefined and then refuses value.toUpperCase(): the question Rust asks.

What happens in Java

Java has no array pattern, so this indexes: parts[1] throws ArrayIndexOutOfBoundsException for the one-item array that split gives back.

What happens in C#

This indexes, it is not a pattern: parts[1] throws IndexOutOfRangeException. C# 11's list pattern, line.Split('=') is [var key, var value], has the shape of Rust's and is a test with an else, so it cannot crash.

What happens in C

strchr returns NULL when there is no =; the layer-1 program checks that, and this version leaves the check out, so eq - line and eq + 1 are arithmetic on a null pointer. Undefined behaviour: on this machine the program died with a segmentation fault.

What happens in C++

find returns npos when there is no = (the layer-1 program checks it; this version does not), and npos + 1 wraps to 0, so the value half is the whole line. The program prints mode -> mode with no error.

What Rust sees

split_once returns an Option: None when the line has no =. A let pattern has to fit every value it can be given, and Some((key, value)) does not fit None, so Rust refuses (E0005) and suggests let ... else.

Why Rust says no

The line mode has no =. In the other languages the check for that is a step you can leave out, and nothing complains; the programs here leave it out and were run with that line: a wrong value, undefined, an exception, or a crash. Careful code checks (the layer-1 programs do), and C# 11's list pattern makes you. Rust makes the check part of the pattern, so leaving it out does not build.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
fn main() {
let line = "mode";
let Some((key, value)) = line.split_once('=') else {
println!("no '=' in {line:?}");
return;
};
println!("{key} -> {value}");
}
$ cargo run
no '=' in "mode"

The toolkit

A pattern is a shape a value can have, with names where you want to keep a part. Matching a value against it either fails or gives each name its part, and taking a value apart this way is called destructuring. Match is where you met patterns (literals, ranges, guards, slices) and this page does not repeat it. It covers the other places a pattern can stand, what a pattern does to ownership, and the small rules (_ against _name, &) that trip people up. Outputs come from Linux on 64-bit ARM with Rust 1.99.1.

Patterns stand in many places

Wherever Rust introduces a variable it accepts a pattern. Each line of this program takes something apart in a different place:

Rustsrc/main.rsRun ↗
struct Point {
x: i32,
y: i32,
}
// a parameter can be a pattern: this takes the point apart
fn manhattan(Point { x, y }: &Point) -> i32 {
x.abs() + y.abs()
}
fn main() {
let p = Point { x: 3, y: -4 };
// let
let Point { x, y } = p;
println!("let: x = {x}, y = {y}");
// function parameter
println!("parameter: {}", manhattan(&p));
// for: each item is split into a pair
let scores = [("ana", 90), ("bo", 72)];
for (name, score) in scores {
println!("for: {name} scored {score}");
}
// if let: only when the shape fits
if let Some((key, value)) = "mode=fast".split_once('=') {
println!("if let: {key} -> {value}");
}
// while let: until the shape stops fitting
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("while let: popped {top}");
}
// let ... else: the shape must fit, or leave
let Some((head, _)) = "a,b".split_once(',') else {
return;
};
println!("let else: head = {head}");
// a closure parameter
let add = |(a, b): (i32, i32)| a + b;
println!("closure: {}", add((2, 5)));
// matches!
println!("matches!: {}", matches!(p, Point { x: 3, .. }));
}
$ cargo run
let: x = 3, y = -4
parameter: 7
for: ana scored 90
for: bo scored 72
if let: mode -> fast
while let: popped 3
while let: popped 2
while let: popped 1
let else: head = a
closure: 7
matches!: true
Place Example Must the pattern always fit?
let let (a, b) = pair; yes
function parameter fn f((a, b): (i32, i32)) yes
closure parameter |(a, b)| a + b yes
for for (i, c) in … yes
match arm Some(n) => … no, but together the arms must cover every value
if let if let Some(n) = o { … } no
while let while let Some(n) = it.next() { … } no
let … else let Some(n) = o else { return }; no, and the else must leave
matches! matches!(o, Some(1 | 2)) no

A closure is a small unnamed function (Closures). Some(..) and Ok(..) come from Option and Errors.

Patterns that must always fit

A pattern that fits every value of its type is irrefutable: (a, b) for a pair, Point { x, y }, _. One that can fail is refutable: Some(n), Ok(n), 1..=5. The last column of the table is the rule. Declarations and types showed the error for let and its fix, let ... else. The other places that need a pattern which always fits say the same, and none of them has an else to add:

Rustsrc/main.rs✗ won't compile
fn first_of(Some(x): Option<i32>) -> i32 {
x
}
fn main() {
let items = [Some(1), None, Some(3)];
for Some(n) in items {
println!("{n}");
}
let double = |Some(n): Option<i32>| n * 2;
println!("{} {}", first_of(Some(1)), double(Some(2)));
}
$ cargo buildWhat is E0005?
error[E0005]: refutable pattern in function argument
 --> src/main.rs:1:13
  |
1 | fn first_of(Some(x): Option<i32>) -> i32 {
  |             ^^^^^^^ pattern `None` not covered
  |
  = note: the matched value is of type `Option<i32>`

error[E0005]: refutable pattern in `for` loop binding
 --> src/main.rs:7:9
  |
7 |     for Some(n) in items {
  |         ^^^^^^^ pattern `None` not covered
  |
  = note: the matched value is of type `Option<i32>`

error[E0005]: refutable pattern in closure argument
  --> src/main.rs:10:19
   |
10 |     let double = |Some(n): Option<i32>| n * 2;
   |                   ^^^^^^^ pattern `None` not covered
   |
   = note: the matched value is of type `Option<i32>`

Take the whole value and look inside it with match or if let. For arrays the compiler also checks the length, which is part of the type:

Rustsrc/main.rs✗ won't compile
fn main() {
let [x, y] = [1, 2, 3];
println!("{x} {y}");
}
$ cargo buildWhat is E0527?
error[E0527]: pattern requires 2 elements but array has 3
 --> src/main.rs:2:9
  |
2 |     let [x, y] = [1, 2, 3];
  |         ^^^^^^ expected 3 elements

Whether a pattern can fail depends on the type, not on how it looks. Ok(x) cannot fail against a Result whose error type has no values (std::convert::Infallible), so let Ok(x) = r; compiles there.

Taking things apart

A struct pattern names fields, in any order. x: px renames, y: 0 tests, and .. skips what you do not name. A tuple or tuple struct pattern goes by position, and .. skips items in the middle or at the end. Patterns nest as deep as the data does:

Rustsrc/main.rsRun ↗
struct Point {
x: i32,
y: i32,
}
struct Rgb(u8, u8, u8);
enum Shape {
Circle { center: Point, radius: u32 },
Rect(Point, Point),
}
fn describe(shape: &Shape) -> String {
match shape {
Shape::Circle {
center: Point { x: 0, y: 0 },
radius,
} => format!("circle at the origin, radius {radius}"),
Shape::Circle { center, radius } => {
format!("circle at ({}, {}), radius {radius}", center.x, center.y)
}
Shape::Rect(Point { x: x1, y: y1 }, Point { x: x2, y: y2 }) => {
format!("rect {} wide, {} high", x2 - x1, y2 - y1)
}
}
}
fn main() {
// by field name, in any order; a new name after the colon
let Point { y, x: px } = Point { x: 3, y: -2 };
println!("px = {px}, y = {y}");
// `..` skips the fields (or items) you do not name
let color = Rgb(200, 100, 50);
let Rgb(red, ..) = color;
let Rgb(_, green, blue) = color;
let (first, .., last) = (1, 2, 3, 4);
println!("red = {red}, green = {green}, blue = {blue}");
println!("first = {first}, last = {last}");
// patterns nest as deep as the data does
let shapes = [
Shape::Circle {
center: Point { x: 0, y: 0 },
radius: 2,
},
Shape::Circle {
center: Point { x: 5, y: 1 },
radius: 3,
},
Shape::Rect(Point { x: 1, y: 1 }, Point { x: 4, y: 3 }),
];
for shape in &shapes {
println!("{}", describe(shape));
}
// an Option of a pair, and a pair of Options
let found = Some(("ana", 30));
if let Some((name, age)) = found {
println!("{name} is {age}");
}
match (Some(1), None::<i32>) {
(Some(a), Some(b)) => println!("both: {a} {b}"),
(Some(a), None) | (None, Some(a)) => println!("exactly one: {a}"),
(None, None) => println!("neither"),
}
}
$ cargo run
px = 3, y = -2
red = 200, green = 100, blue = 50
first = 1, last = 4
circle at the origin, radius 2
circle at (5, 1), radius 3
rect 3 wide, 2 high
ana is 30
exactly one: 1

A struct pattern has to mention every field or end in ... Leaving one out is an error:

Rustsrc/main.rs✗ won't compile
struct Point {
x: i32,
y: i32,
z: i32,
}
fn main() {
let p = Point { x: 1, y: 2, z: 3 };
// a struct pattern must mention every field, or end in `..`
let Point { x } = p;
println!("x = {x}");
}
$ cargo buildWhat is E0027?
error[E0027]: pattern does not mention fields `y`, `z`
  --> src/main.rs:10:9
   |
10 |     let Point { x } = p;
   |         ^^^^^^^^^^^ missing fields `y`, `z`
   |
help: include the missing fields in the pattern
   |
10 |     let Point { x, y, z } = p;
   |                  ++++++
help: if you don't care about these missing fields, you can explicitly ignore them
   |
10 |     let Point { x, y: _, z: _ } = p;
   |                  ++++++++++++
help: or always ignore missing fields here
   |
10 |     let Point { x, .. } = p;
   |                  ++++

A slice pattern fits an array or a slice, not a Vec (Match has the shapes). Slice the Vec first, with as_slice() or [..]; the rest you get is a borrowed slice, not a new list. The RPN exercise below reads its stack with stack[..]:

Rustsrc/main.rsRun ↗
fn main() {
// a slice pattern needs an array or a slice, so slice the Vec first
let words = vec!["a", "b", "c"];
if let [first, rest @ ..] = words.as_slice() {
println!("first = {first}, rest = {rest:?}");
}
let mut stack = vec![7];
for _ in 0..2 {
match stack[..] {
[] => println!("empty"),
[only] => println!("one value: {only}"),
[.., top] => println!("top of {}: {top}", stack.len()),
}
stack.push(8);
}
}
$ cargo run
first = a, rest = ["b", "c"]
one value: 7
top of 2: 8

Alternatives and @

Match showed | and @ at the top of a pattern. Both also work inside another pattern, so Some(1 | 2) is shorter than two arms. Every alternative must bind the same names with the same types, or the compiler says which one is missing:

Rustsrc/main.rsRun ↗
fn size(n: Option<u32>) -> String {
match n {
// an or-pattern can sit inside another pattern
Some(1 | 2) => String::from("one or two"),
// `@` names the whole value and still tests the shape
Some(small @ 3..=9) => format!("small: {small}"),
Some(other) => format!("big: {other}"),
None => String::from("missing"),
}
}
fn main() {
for n in [Some(2), Some(5), Some(50), None] {
println!("{n:?} -> {}", size(n));
}
// both sides of | must bind the same names, with the same types
let results = [Ok(3), Err(4)];
for r in results {
let (Ok(n) | Err(n)) = r;
println!("{r:?} -> n = {n}");
}
}
$ cargo run
Some(2) -> one or two
Some(5) -> small: 5
Some(50) -> big: 50
None -> missing
Ok(3) -> n = 3
Err(4) -> n = 4
Rustsrc/main.rs✗ won't compile
fn main() {
let pair = (Some(1), Some(2));
match pair {
// `a` is bound on the left of the `|` but not on the right
(Some(a), _) | (_, None) => println!("matched"),
_ => println!("other"),
}
}
$ cargo buildWhat is E0408?
error[E0408]: variable `a` is not bound in all patterns
 --> src/main.rs:5:24
  |
5 |         (Some(a), _) | (_, None) => println!("matched"),
  |               -        ^^^^^^^^^ pattern doesn't bind `a`
  |               |
  |               variable not in all patterns

A name always binds

A name in a pattern is a new variable, not a comparison; the exception is a const, which is compared (Match). That includes the name of an enum variant written without its path, which the compiler catches (E0170, shown in Enums). A static is not compared either. A name that matches a static would be read as a new binding, and the compiler refuses it (E0530):

Rustsrc/main.rs✗ won't compile
static LIMIT: u32 = 10;
fn main() {
let spent = 12;
match spent {
LIMIT => println!("at the limit"),
_ => println!("elsewhere"),
}
}
$ cargo buildWhat is E0530?
error[E0530]: match bindings cannot shadow statics
 --> src/main.rs:6:9
  |
1 | static LIMIT: u32 = 10;
  | ----------------------- the static `LIMIT` is defined here
...
6 |         LIMIT => println!("at the limit"),
  |         ^^^^^ cannot be named the same as a static

_ ignores, _name binds

_ is a pattern that fits anything and binds nothing. A name that starts with an underscore is a normal variable that only tells the compiler not to warn when it is unused. The difference shows when the value is not Copy: _ leaves it where it is, and _name moves it. Inside a pattern, let (_, right) = pair; moves only the right half.

Rustsrc/main.rsRun ↗
fn main() {
let s = String::from("hello");
let pair = (String::from("left"), String::from("right"));
let _ = s; // `_` does not bind, so nothing moves
println!("after `let _ = s`, s is still {s}");
let (_, right) = pair; // only the right half moves
println!("after `let (_, right) = pair`, pair.0 is still {}", pair.0);
println!("right is {right}");
}
$ cargo run
after `let _ = s`, s is still hello
after `let (_, right) = pair`, pair.0 is still left
right is right
Rustsrc/main.rs✗ won't compile
fn main() {
let s = String::from("hello");
let _unused = s; // a name starting with `_` is still a name: it binds
println!("{s}");
}
$ cargo buildWhat is E0382?
error[E0382]: borrow of moved value: `s`
 --> src/main.rs:4:16
  |
2 |     let s = String::from("hello");
  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 |     let _unused = s; // a name starting with `_` is still a name: it binds
  |                   - value moved here
4 |     println!("{s}");
  |                ^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let _unused = s.clone(); // a name starting with `_` is still a name: it binds
  |                    ++++++++

References in patterns

When you match a reference against a pattern that is not a reference, Rust looks through the reference and makes each name a reference too. The name’s type follows the value: a copy for a value, &T for &value, &mut T for &mut value. These are called the default binding modes, also known as match ergonomics. This program prints the type of each name:

Rustsrc/main.rsRun ↗
fn show<T>(label: &str, _value: &T) {
println!("{label}: {}", std::any::type_name::<T>());
}
fn main() {
let mut n = Some(5);
let pair = (1, 2);
// the value itself: the name gets a copy
if let Some(x) = n {
show("if let Some(x) = n", &x);
}
// a shared reference to the value: the name gets a shared reference
if let Some(x) = &n {
show("if let Some(x) = &n", &x);
}
// a mutable reference: the name gets a mutable reference
if let Some(x) = &mut n {
*x += 1;
show("if let Some(x) = &mut n", &x);
}
println!("n is now {n:?}");
// the same for tuples
let (a, _) = &pair;
show("let (a, _) = &pair", &a);
// a & in the pattern strips the reference by hand
let &(b, _) = &pair;
show("let &(b, _) = &pair", &b);
}
$ cargo run
if let Some(x) = n: i32
if let Some(x) = &n: &i32
if let Some(x) = &mut n: &mut i32
n is now Some(6)
let (a, _) = &pair: &i32
let &(b, _) = &pair: i32
You match The names are Use it to
value copies, or moved parts take the value apart and use it up
&value &T into the value read parts and keep the value
&mut value &mut T into the value change parts in place

A & in a pattern goes the other way: it removes one reference by hand, so the name is the value behind it. That is handy for a Copy part such as a number. for x in &v and for &x in &v differ in just this way (Looping Over a Collection explains the loop forms):

Rustsrc/main.rsRun ↗
fn show<T>(label: &str, _value: &T) {
println!("{label}: {}", std::any::type_name::<T>());
}
fn main() {
let pairs = vec![(1, 10), (2, 20)];
// `first` gives an Option holding a reference to the first pair
// no &: the pattern passes through the reference, so a and b are references
if let Some((a, b)) = pairs.first() {
show("Some((a, b)), a", &a);
show("Some((a, b)), b", &b);
}
// with &: the pattern strips the reference, so a and b are plain numbers
if let Some(&(a, b)) = pairs.first() {
show("Some(&(a, b)), a", &a);
show("Some(&(a, b)), b", &b);
}
// in a loop it is the same choice
let mut total = 0;
for &(a, b) in &pairs {
total += a * b;
}
println!("total: {total}");
}
$ cargo run
Some((a, b)), a: &i32
Some((a, b)), b: &i32
Some(&(a, b)), a: i32
Some(&(a, b)), b: i32
total: 50

A related surprise: a &i32 cannot be compared with 60.

Rustsrc/main.rs✗ won't compile
fn main() {
let scores = vec![40, 75, 90];
for score in &scores {
if score > 60 {
println!("{score} passes");
}
}
}
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:4:20
  |
4 |         if score > 60 {
  |                    ^^ expected `&_`, found integer
  |
  = note: expected reference `&_`
                  found type `{integer}`
help: consider dereferencing the borrow
  |
4 |         if *score > 60 {
  |            +

Write *score > 60, or for &score in &scores. Closure parameters are patterns too, which is why |&x| and |&&x| appear in iterator chains (Iterators explains the references being taken off). For a part that is not Copy, a & pattern cannot work, because it would move the part out of a borrowed value:

Rustsrc/main.rs✗ won't compile
fn main() {
let names = vec![String::from("ana"), String::from("bo")];
for &name in &names {
println!("{name}");
}
}
$ cargo buildWhat is E0507?
error[E0507]: cannot move out of a shared reference
 --> src/main.rs:3:18
  |
3 |     for &name in &names {
  |          ----    ^^^^^^
  |          |
  |          data moved here because `name` has type `String`, which does not implement the `Copy` trait
  |
help: consider removing the borrow
  |
3 -     for &name in &names {
3 +     for name in &names {

Does a pattern move, copy or borrow?

Matching a value by value moves each part that is not Copy into its name, and the value can no longer be used whole. A struct can be partly moved. (The compiler suggests ref name, the older spelling of “borrow this part”, explained below.)

Rustsrc/main.rs✗ won't compile
struct Person {
name: String,
age: u32,
}
fn main() {
let p = Person {
name: String::from("Ana"),
age: 30,
};
let Person { name, age } = p;
println!("{name} is {age}");
println!("p.name is still {}", p.name);
}
$ cargo buildWhat is E0382?
error[E0382]: borrow of moved value: `p.name`
  --> src/main.rs:13:36
   |
11 |     let Person { name, age } = p;
   |                  ---- value moved here
12 |     println!("{name} is {age}");
13 |     println!("p.name is still {}", p.name);
   |                                    ^^^^^^ value borrowed here after move
   |
   = note: move occurs because `p.name` has type `String`, which does not implement the `Copy` trait
help: borrow this binding in the pattern to avoid moving the value
   |
11 |     let Person { ref name, age } = p;
   |                  +++

A frequent case is an Option<String> in an if let:

Rustsrc/main.rs✗ won't compile
fn main() {
let nick: Option<String> = Some(String::from("ana"));
if let Some(n) = nick {
println!("hello, {n}");
}
println!("nick is {nick:?}");
}
$ cargo buildWhat is E0382?
error[E0382]: borrow of partially moved value: `nick`
 --> src/main.rs:6:24
  |
3 |     if let Some(n) = nick {
  |                 - value partially moved here
...
6 |     println!("nick is {nick:?}");
  |                        ^^^^ value borrowed here after partial move
  |
  = note: partial move occurs because value has type `String`, which does not implement the `Copy` trait
help: borrow this binding in the pattern to avoid moving the value
  |
3 |     if let Some(ref n) = nick {
  |                 +++

Match a reference instead: &nick, or nick.as_ref(), which makes an Option<&String> (Option::as_ref). Either way n is a &String, and &mut nick lets the arm change the text in place:

Rustsrc/main.rsRun ↗
fn main() {
let mut nick: Option<String> = Some(String::from("ana"));
// match on a reference: `n` is a &String and nick stays whole
if let Some(n) = &nick {
println!("hello, {n}");
}
// as_ref() makes the same Option<&String> as a method call
if let Some(n) = nick.as_ref() {
println!("hello again, {n}");
}
// a mutable reference lets the arm change the String in place
if let Some(n) = &mut nick {
n.push_str("!!");
}
println!("nick is {nick:?}");
}
$ cargo run
hello, ana
hello again, ana
nick is Some("ana!!")

To compare text inside an Option<String> with string literals, as_deref() makes an Option<&str> without moving anything (Option::as_deref):

Rustsrc/main.rsRun ↗
fn greeting(nick: &Option<String>) -> String {
// as_deref turns &Option<String> into Option<&str>, which string patterns can test
match nick.as_deref() {
Some("ana") => String::from("hello, ana"),
Some(other) => format!("hi, {other}"),
None => String::from("hello, stranger"),
}
}
fn main() {
for nick in [Some(String::from("ana")), Some(String::from("bo")), None] {
println!("{nick:?} -> {}", greeting(&nick));
}
}
$ cargo run
Some("ana") -> hello, ana
Some("bo") -> hi, bo
None -> hello, stranger

Before default binding modes, the way to borrow a part was ref (and ref mut) in front of the name, as the compiler’s hint in the first error shows. It still works on a value that is not behind a reference:

Rustsrc/main.rsRun ↗
struct Person {
name: String,
age: u32,
}
fn main() {
let p = Person {
name: String::from("Ana"),
age: 30,
};
// `ref` borrows the field instead of moving it out
let Person { ref name, age } = p;
println!("ref: {name} is {age}");
// matching on a reference borrows too, without `ref`; here `age` is a &u32 as well
let Person { name, age } = &p;
println!("&p: {name} is {age}");
// p is still whole, so it can be used or moved afterwards
let moved = p;
println!("moved: {}", moved.name);
}
$ cargo run
ref: Ana is 30
&p: Ana is 30
moved: Ana

&p borrows the same way without ref, but every name, age too, is then a reference.

When the compiler complains

Error What happened Fix
E0005 a pattern that can fail stands where it must always fit if let, let ... else or match
E0027 a struct pattern left a field out name it, or end with ..
E0527 an array pattern has the wrong number of items match the length, or use ..
E0408 the sides of a | bind different names bind the same names on every side
E0170 a bare name that looks like a variant binds instead write the path, or use the variants
E0530 a static used as a pattern use a const, or a guard
E0382 a pattern moved a part out, then the whole value was used match a reference, or ref
E0507 a & pattern tried to move out of a borrow drop the &, or clone
E0308 &i32 met i32 *x, or a & pattern

Going deeper

What a binding does in memory

A pattern by value and a pattern through a reference build different things. The program prints what the two diagrams show:

Rustsrc/main.rsRun ↗
struct Person {
name: String,
age: u32,
}
fn main() {
// the pattern takes `p` apart by value
let p = Person {
name: String::from("Ana"),
age: 30,
};
let heap_text = p.name.as_ptr();
let Person { name, age } = p;
println!(
"by value: name is the same heap text: {}",
name.as_ptr() == heap_text
);
println!("by value: name len {}, cap {}", name.len(), name.capacity());
println!("by value: age is a copy, p.age is still {}", p.age);
println!("by value: age is {age}");
// the pattern looks at `q` through a reference
let q = Person {
name: String::from("Bo"),
age: 25,
};
println!(
"by reference: q.name len {}, cap {}",
q.name.len(),
q.name.capacity()
);
let Person { name, age } = &q;
println!(
"by reference: name points at q.name: {}",
std::ptr::eq(name, &q.name)
);
println!(
"by reference: age points at q.age: {}",
std::ptr::eq(age, &q.age)
);
}
$ cargo run
by value: name is the same heap text: true
by value: name len 3, cap 3
by value: age is a copy, p.age is still 30
by value: age is 30
by reference: q.name len 2, cap 2
by reference: name points at q.name: true
by reference: age points at q.age: true
let Person { name, age } = p;
STACKHEAPp: Person (name moved out)name: ptr, len 3, cap 3age 30p.age can still be read, because a number is copiedname: Stringptrlen 3cap 3age: u32303 bytes of UTF-8Ana
The text did not move: name holds the same heap address (the program prints true). Only the String's three words were handed over, and p can no longer be used whole.
let Person { name, age } = &q;
STACKHEAPq: Personname: ptr, len 2, cap 2age 25name: &String&q.nameage: &u32&q.age2 bytes of UTF-8Bo
Both names point into q, not at copies (the program prints true for each). q is untouched and whole.

Two rules that live elsewhere

When a value that nothing holds is dropped, and why let _ = lock.lock() is refused, are in Ownership. Some pattern rules depend on the edition set in Cargo.toml: a let chain (if let Some(n) = v && n > 5) needs edition 2024 (If / else runs it under both), and the Edition Guide lists where edition 2024 reserves ref, mut and & under a default binding mode.

Practice

Try it

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

Describe a shape

Take a Shape apart: a circle, a rectangle given by two corners, and a tag that owns a String. describe takes &Shape, so the names your patterns bind are references; grow takes &mut Shape, so they need a *. The checks include corners in either order, empty shapes, and the shapes afterwards.

Starterexercises/describe.rs
struct Point {
x: i32,
y: i32,
}
enum Shape {
Circle { center: Point, radius: u32 },
Rect(Point, Point),
Tag { name: String, at: Point },
}
/// Circle: "circle r=2 at (0, 0)". Rect (the corners can come in either order): "rect 3x2",
/// width then height. Tag: "tag ana at (5, -1)". A circle with radius 0, or a rect with
/// no width or no height, is "empty".
fn describe(shape: &Shape) -> String {
todo!("match on the shape; each arm names the fields it needs")
}
/// Make a shape bigger in place: a circle's radius goes up by 1, and a rect's second corner
/// moves 1 right and 1 down (x + 1, y + 1). A tag does not change.
fn grow(shape: &mut Shape) {
todo!("names bound through &mut are &mut references: write *radius += 1")
}
fn main() {
let at = |x, y| Point { x, y };
let mut shapes = vec![
Shape::Circle {
center: at(0, 0),
radius: 2,
},
Shape::Rect(at(1, 1), at(4, 3)),
Shape::Rect(at(4, 3), at(1, 1)),
Shape::Tag {
name: String::from("ana"),
at: at(5, -1),
},
Shape::Circle {
center: at(0, 0),
radius: 0,
},
Shape::Rect(at(2, 2), at(2, 9)),
];
let before: Vec<String> = shapes.iter().map(describe).collect();
assert_eq!(
before,
[
"circle r=2 at (0, 0)",
"rect 3x2",
"rect 3x2",
"tag ana at (5, -1)",
"empty",
"empty",
]
);
// describing only borrows: the tag still owns its name afterwards
assert_eq!(describe(&shapes[3]), "tag ana at (5, -1)");
for shape in &mut shapes {
grow(shape);
}
let after: Vec<String> = shapes.iter().map(describe).collect();
assert_eq!(
after,
[
"circle r=3 at (0, 0)",
"rect 4x3",
"rect 2x1",
"tag ana at (5, -1)",
"circle r=1 at (0, 0)",
"rect 1x8",
]
);
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/describe.solution.rsRun ↗
struct Point {
x: i32,
y: i32,
}
enum Shape {
Circle { center: Point, radius: u32 },
Rect(Point, Point),
Tag { name: String, at: Point },
}
/// Circle: "circle r=2 at (0, 0)". Rect (the corners can come in either order): "rect 3x2",
/// width then height. Tag: "tag ana at (5, -1)". A circle with radius 0, or a rect with
/// no width or no height, is "empty".
fn describe(shape: &Shape) -> String {
match shape {
Shape::Circle { radius: 0, .. } => String::from("empty"),
Shape::Circle {
center: Point { x, y },
radius,
} => format!("circle r={radius} at ({x}, {y})"),
Shape::Rect(Point { x: x1, y: y1 }, Point { x: x2, y: y2 }) => {
let (w, h) = (x1.abs_diff(*x2), y1.abs_diff(*y2));
if w == 0 || h == 0 {
String::from("empty")
} else {
format!("rect {w}x{h}")
}
}
Shape::Tag {
name,
at: Point { x, y },
} => format!("tag {name} at ({x}, {y})"),
}
}
/// Make a shape bigger in place: a circle's radius goes up by 1, and a rect's second corner
/// moves 1 right and 1 down (x + 1, y + 1). A tag does not change.
fn grow(shape: &mut Shape) {
match shape {
Shape::Circle { radius, .. } => *radius += 1,
Shape::Rect(_, Point { x, y }) => {
*x += 1;
*y += 1;
}
Shape::Tag { .. } => {}
}
}
fn main() {
let at = |x, y| Point { x, y };
let mut shapes = vec![
Shape::Circle {
center: at(0, 0),
radius: 2,
},
Shape::Rect(at(1, 1), at(4, 3)),
Shape::Rect(at(4, 3), at(1, 1)),
Shape::Tag {
name: String::from("ana"),
at: at(5, -1),
},
Shape::Circle {
center: at(0, 0),
radius: 0,
},
Shape::Rect(at(2, 2), at(2, 9)),
];
let before: Vec<String> = shapes.iter().map(describe).collect();
assert_eq!(
before,
[
"circle r=2 at (0, 0)",
"rect 3x2",
"rect 3x2",
"tag ana at (5, -1)",
"empty",
"empty",
]
);
// describing only borrows: the tag still owns its name afterwards
assert_eq!(describe(&shapes[3]), "tag ana at (5, -1)");
for shape in &mut shapes {
grow(shape);
}
let after: Vec<String> = shapes.iter().map(describe).collect();
assert_eq!(
after,
[
"circle r=3 at (0, 0)",
"rect 4x3",
"rect 2x1",
"tag ana at (5, -1)",
"circle r=1 at (0, 0)",
"rect 1x8",
]
);
println!("all checks passed");
}
$ cargo run
all checks passed

Borrow the nickname

Person has an Option<String> nickname. Return the nickname, or the name, borrowed from the person. match person.nick would move the String out of a borrow, so match a reference (as_deref also works). The checks include non-ASCII text, a tie, and an answer that must point into the person, not at a copy.

Starterexercises/nicknames.rs
struct Person {
name: String,
nick: Option<String>,
}
/// The nickname if there is one, otherwise the name. Borrow it from the person: do not clone.
fn display_name(person: &Person) -> &str {
todo!("match &person.nick: matching person.nick itself would try to move the String out")
}
/// The longest nickname (counting characters, not bytes), borrowed from the people.
/// The first one wins a tie. None if nobody has a nickname.
fn longest_nick(people: &[Person]) -> Option<&str> {
todo!("loop over the people and bind each nickname through a reference")
}
fn main() {
let person = |name: &str, nick: Option<&str>| Person {
name: String::from(name),
nick: nick.map(String::from),
};
let people = vec![
person("Ana", Some("annie")),
person("Bo", None),
person("Cy", Some("ñññ")),
person("Di", Some("abcd")),
person("Ed", Some("wxyz")),
];
assert_eq!(display_name(&people[0]), "annie");
assert_eq!(display_name(&people[1]), "Bo");
// the answer points into the person: nothing was copied or moved out
let shown = display_name(&people[0]);
assert!(std::ptr::eq(
shown.as_ptr(),
people[0].nick.as_ref().unwrap().as_ptr()
));
assert_eq!(longest_nick(&people), Some("annie"));
// "ñññ" is 3 characters but 6 bytes, and "abcd" and "wxyz" tie at 4: the first wins
assert_eq!(longest_nick(&people[2..]), Some("abcd"));
assert_eq!(longest_nick(&people[1..2]), None);
assert_eq!(longest_nick(&[]), None);
// the people are still whole afterwards
assert_eq!(people[0].nick.as_deref(), Some("annie"));
assert_eq!(people[3].name, "Di");
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/nicknames.solution.rsRun ↗
struct Person {
name: String,
nick: Option<String>,
}
/// The nickname if there is one, otherwise the name. Borrow it from the person: do not clone.
fn display_name(person: &Person) -> &str {
match &person.nick {
Some(nick) => nick,
None => &person.name,
}
}
/// The longest nickname (counting characters, not bytes), borrowed from the people.
/// The first one wins a tie. None if nobody has a nickname.
fn longest_nick(people: &[Person]) -> Option<&str> {
let mut best: Option<&str> = None;
for person in people {
if let Some(nick) = &person.nick {
let longer = match best {
Some(current) => nick.chars().count() > current.chars().count(),
None => true,
};
if longer {
best = Some(nick);
}
}
}
best
}
fn main() {
let person = |name: &str, nick: Option<&str>| Person {
name: String::from(name),
nick: nick.map(String::from),
};
let people = vec![
person("Ana", Some("annie")),
person("Bo", None),
person("Cy", Some("ñññ")),
person("Di", Some("abcd")),
person("Ed", Some("wxyz")),
];
assert_eq!(display_name(&people[0]), "annie");
assert_eq!(display_name(&people[1]), "Bo");
// the answer points into the person: nothing was copied or moved out
let shown = display_name(&people[0]);
assert!(std::ptr::eq(
shown.as_ptr(),
people[0].nick.as_ref().unwrap().as_ptr()
));
assert_eq!(longest_nick(&people), Some("annie"));
// "ñññ" is 3 characters but 6 bytes, and "abcd" and "wxyz" tie at 4: the first wins
assert_eq!(longest_nick(&people[2..]), Some("abcd"));
assert_eq!(longest_nick(&people[1..2]), None);
assert_eq!(longest_nick(&[]), None);
// the people are still whole afterwards
assert_eq!(people[0].nick.as_deref(), Some("annie"));
assert_eq!(people[3].name, "Di");
println!("all checks passed");
}
$ cargo run
all checks passed

Reverse Polish

Evaluate [Num(3), Num(4), Op('+')]: numbers go on a stack, and an operator pops two. The token slice is borrowed, so the names your pattern binds are references (*n gets the number). Use let (Some(right), Some(left)) = (stack.pop(), stack.pop()) else { … } (two ? also work), and match stack[..] at the end, as in the slice example above. The checks include the order of the operands, too few operands, a division by zero and leftover values; ignore overflow.

Starterexercises/rpn.rs
enum Token {
Num(i64),
Op(char),
}
/// Evaluate a reverse Polish expression: numbers go on a stack, and an operator
/// takes the top two (the older one is the left operand) and pushes the result.
/// None for: a missing operand, an operator other than + - * /, a division by
/// zero, or anything but exactly one value left at the end.
fn eval(tokens: &[Token]) -> Option<i64> {
todo!("match each token; let ... else for the two operands; match stack[..] at the end")
}
fn main() {
use Token::{Num, Op};
assert_eq!(eval(&[Num(3), Num(4), Op('+')]), Some(7));
assert_eq!(eval(&[Num(2), Num(3), Op('-')]), Some(-1));
assert_eq!(eval(&[Num(8), Num(2), Op('/')]), Some(4));
// (5 + ((1 + 2) * 4)) - 3
let long = [
Num(5),
Num(1),
Num(2),
Op('+'),
Num(4),
Op('*'),
Op('+'),
Num(3),
Op('-'),
];
assert_eq!(eval(&long), Some(14));
assert_eq!(eval(&[Num(7)]), Some(7));
assert_eq!(eval(&[]), None);
assert_eq!(eval(&[Num(1), Num(2)]), None);
assert_eq!(eval(&[Op('+')]), None);
assert_eq!(eval(&[Num(1), Op('+')]), None);
assert_eq!(eval(&[Num(1), Num(0), Op('/')]), None);
assert_eq!(eval(&[Num(1), Num(2), Op('^')]), None);
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/rpn.solution.rsRun ↗
enum Token {
Num(i64),
Op(char),
}
/// Evaluate a reverse Polish expression: numbers go on a stack, and an operator
/// takes the top two (the older one is the left operand) and pushes the result.
/// None for: a missing operand, an operator other than + - * /, a division by
/// zero, or anything but exactly one value left at the end.
fn eval(tokens: &[Token]) -> Option<i64> {
let mut stack: Vec<i64> = Vec::new();
for token in tokens {
match token {
Token::Num(n) => stack.push(*n),
Token::Op(op) => {
let (Some(right), Some(left)) = (stack.pop(), stack.pop()) else {
return None;
};
let value = match op {
'+' => left + right,
'-' => left - right,
'*' => left * right,
'/' if right != 0 => left / right,
_ => return None,
};
stack.push(value);
}
}
}
match stack[..] {
[result] => Some(result),
_ => None,
}
}
fn main() {
use Token::{Num, Op};
assert_eq!(eval(&[Num(3), Num(4), Op('+')]), Some(7));
assert_eq!(eval(&[Num(2), Num(3), Op('-')]), Some(-1));
assert_eq!(eval(&[Num(8), Num(2), Op('/')]), Some(4));
// (5 + ((1 + 2) * 4)) - 3
let long = [
Num(5),
Num(1),
Num(2),
Op('+'),
Num(4),
Op('*'),
Op('+'),
Num(3),
Op('-'),
];
assert_eq!(eval(&long), Some(14));
assert_eq!(eval(&[Num(7)]), Some(7));
assert_eq!(eval(&[]), None);
assert_eq!(eval(&[Num(1), Num(2)]), None);
assert_eq!(eval(&[Op('+')]), None);
assert_eq!(eval(&[Num(1), Op('+')]), None);
assert_eq!(eval(&[Num(1), Num(0), Op('/')]), None);
assert_eq!(eval(&[Num(1), Num(2), Op('^')]), None);
println!("all checks passed");
}
$ cargo run
all checks passed

Read more