Pattern Matching
25 min read
Pattern matching is the feature most TypeScript/JavaScript developers come to miss the moment they go back. Rust’s match is array/object destructuring, a switch statement, type narrowing, and an exhaustiveness checker all rolled into one expression — and the compiler refuses to let you forget a case.
Quick Overview
Section titled “Quick Overview”A pattern describes the shape of a value: a literal, a range, a tuple, a struct, an enum variant, or a combination of these. Rust lets you match a value against patterns in many places — most prominently the match expression, but also let, if let, let ... else, while let, and function parameters. The defining feature is exhaustiveness: a match must cover every possible value, so the compiler turns “I forgot the null case” from a 3 a.m. production incident into a build error.
Note: This file focuses on the patterns themselves and the places they appear —
match,let,if let,let else,while let, and the pattern syntax for tuples, structs, enums, ranges, guards, and bindings. The data types you match on each have their own file: structs, enums, andOption<T>. Method syntax lives in impl blocks.
TypeScript/JavaScript Example
Section titled “TypeScript/JavaScript Example”TypeScript has no real pattern matching. You reach for a combination of destructuring, switch, and manual if/else chains on a discriminant field — and nothing forces you to handle every case.
// A discriminated union — the closest TS analogue to a Rust enum.type Shape = | { kind: "circle"; radius: number } | { kind: "rectangle"; width: number; height: number } | { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "rectangle": return shape.width * shape.height; case "triangle": return 0.5 * shape.base * shape.height; // With this explicit `: number` return type, TS DOES flag a missing case // here (error TS2366: function lacks ending return statement) — no `never` // trick required. The `never`-assignment exhaustiveness trick is only needed // for side-effect (void) switches, where TS will NOT catch a missing case. }}
// Destructuring is separate from "matching":const point = { x: 3, y: 7 };const { x, y } = point; // object destructuringconst [first, , third] = [1, 2, 3]; // array destructuring, skip middle
// Narrowing a value still needs hand-written conditionals:function classifyStatus(code: number): string { if (code === 200) return "OK"; if (code === 301 || code === 302) return "Redirect"; if (code >= 400 && code <= 499) return "Client error"; if (code >= 500 && code <= 599) return "Server error"; return "Other";}In TypeScript, destructuring (pulling values out) and matching (deciding which branch) are two unrelated tools. Exhaustiveness checking is partial: for a value-returning switch with a declared or inferred return type, TypeScript flags a missing case automatically — either at the function (error TS2366, the explicit-return-type case shown above) or at the call site (the inferred-number | undefined case, error TS2322). But it relies on return-type flow analysis, so a side-effect (void) switch will silently miss a case unless you add the never-assignment trick by hand. Rust, as we will see, enforces exhaustiveness structurally for every match — no return type or assert needed.
Rust Equivalent
Section titled “Rust Equivalent”In Rust, destructuring and matching are the same mechanism. A pattern simultaneously tests a value’s shape and binds its parts to names. And match is exhaustive by construction.
#[derive(Debug)]enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, Triangle { base: f64, height: f64 },}
fn area(shape: &Shape) -> f64 { // Every variant must be handled, or this will not compile. match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, Shape::Triangle { base, height } => 0.5 * base * height, }}
fn main() { // `let` itself takes a pattern — this is destructuring. let (name, age) = ("Alice", 30); println!("{name} is {age}");
// Struct destructuring with a `let` pattern. #[derive(Debug)] struct Point { x: i32, y: i32 } let point = Point { x: 3, y: 7 }; let Point { x, y } = point; println!("x={x}, y={y}");
let shapes = [ Shape::Circle { radius: 2.0 }, Shape::Rectangle { width: 3.0, height: 4.0 }, Shape::Triangle { base: 6.0, height: 2.0 }, ]; for s in &shapes { println!("{s:?} -> area {:.2}", area(s)); }}Output (real, from cargo run):
Alice is 30x=3, y=7Circle { radius: 2.0 } -> area 12.57Rectangle { width: 3.0, height: 4.0 } -> area 12.00Triangle { base: 6.0, height: 2.0 } -> area 6.00Note: The
f64literals print as2.0, not2, because Rust’sDebugformatting for floats always shows the decimal point. This is unlike JavaScript, whereconsole.log(2.0)prints2.
Detailed Explanation
Section titled “Detailed Explanation”match is an expression, not a statement
Section titled “match is an expression, not a statement”A JavaScript switch is a statement: it runs side effects, and you return or break out. A Rust match is an expression — it evaluates to a value you can assign or return directly. Each arm is pattern => expression.
fn main() { let score = 84; let grade = match score { 90..=100 => 'A', 80..=89 => 'B', 70..=79 => 'C', _ => 'F', }; println!("grade = {grade}"); // grade = B}Because match is an expression, there is no fall-through and no break. Exactly one arm runs, and its value becomes the value of the whole match. (Compare to switch, where a forgotten break causes accidental fall-through — a class of bug that simply cannot exist here.)
Patterns test and bind at once
Section titled “Patterns test and bind at once”Each arm’s left side is a pattern. When the pattern matches, any names inside it are bound to the corresponding pieces of the value. In Shape::Rectangle { width, height }, the arm matches only Rectangle values, and on a match it binds width and height to that rectangle’s fields. There is no separate “destructure” step.
Literal, range, and or-patterns
Section titled “Literal, range, and or-patterns”Patterns can be concrete values, inclusive ranges (..=), or alternatives joined with |:
fn describe_status(code: u16) -> &'static str { match code { 200 => "OK", 301 | 302 | 307 | 308 => "Redirect", // or-pattern 400..=499 => "Client error", // inclusive range 500..=599 => "Server error", _ => "Other", // wildcard catch-all }}
fn main() { for code in [200u16, 302, 404, 503, 100] { println!("{code} -> {}", describe_status(code)); }}Output (real):
200 -> OK302 -> Redirect404 -> Client error503 -> Server error100 -> OtherThe _ wildcard matches anything and binds nothing — it is the equivalent of a default: case, and it is how you satisfy exhaustiveness for the “everything else” bucket.
Match guards: a pattern plus a condition
Section titled “Match guards: a pattern plus a condition”A pattern can be followed by if <condition>, called a match guard. The arm matches only if the pattern fits and the guard is true.
#[derive(Debug)]struct Point { x: i32, y: i32 }
fn classify(point: Point) -> &'static str { match point { Point { x: 0, y: 0 } => "origin", Point { x: 0, y } if y > 0 => "north axis", Point { x: 0, y: _ } => "south axis", Point { x, y: 0 } if x != 0 => "horizontal axis", Point { .. } => "somewhere else", }}Here Point { x: 0, y } matches any point on the y-axis and binds the y-coordinate; the guard if y > 0 then narrows it further. The Point { .. } pattern uses .. to ignore all remaining fields.
Binding with @
Section titled “Binding with @”Sometimes you want to test a value against a range and also keep the value. The @ operator binds a name to a value while also pattern-matching it:
fn main() { let id = 42; match id { n @ 1..=50 => println!("{n} is in the low range"), n @ 51..=100 => println!("{n} is in the high range"), n => println!("{n} is out of range"), } // 42 is in the low range}Without @, the range pattern 1..=50 would match but give you no name for the matched value; with n @ 1..=50 you get both.
Reference patterns: matching without moving
Section titled “Reference patterns: matching without moving”This is the part that trips up TypeScript developers, because JavaScript has no concept of ownership. When you match a value by reference (match &thing), the bindings inside the pattern are also references, so the original value is not consumed:
fn main() { let owned = String::from("config.toml"); let maybe = Some(owned);
// Matching `&maybe` borrows; `path` is a `&String`, not a moved String. match &maybe { Some(path) => println!("path is {path}"), None => println!("no path"), }
// We can still use `maybe` afterwards because nothing was moved out. println!("still own it: {maybe:?}");}Output (real):
path is config.tomlstill own it: Some("config.toml")Modern Rust applies match ergonomics: when you match a reference (&maybe) against a non-reference pattern (Some(path)), the compiler automatically makes the inner bindings references for you. You rarely need to write the old ref keyword anymore. (See Ownership for the underlying move/borrow rules.)
Tuple, slice, and nested patterns
Section titled “Tuple, slice, and nested patterns”Patterns nest arbitrarily, mirroring the structure of the data.
fn main() { // Tuple destructuring, including nested tuples. let record = ("Bob", (98.5, 87.0), true); let (name, (math, science), active) = record; println!("{name}: math={math}, science={science}, active={active}");
// `..` ignores the middle of a tuple. let numbers = (1, 2, 3, 4, 5); let (first, .., last) = numbers; println!("first={first}, last={last}");
// Slice patterns match on the *shape* of a slice. let parts: &[&str] = &["GET", "/index.html", "HTTP/1.1"]; match parts { [method, path, version] => println!("{method} {path} ({version})"), [method, path] => println!("{method} {path}"), [] => println!("empty request line"), _ => println!("unexpected request line"), }
// Bind the head and capture the rest with `tail @ ..`. let words = vec!["a", "b", "c", "d"]; if let [head, tail @ ..] = words.as_slice() { println!("head={head}, tail={tail:?}"); }}Output (real):
Bob: math=98.5, science=87, active=truefirst=1, last=5GET /index.html (HTTP/1.1)head=a, tail=["b", "c", "d"]Slice patterns ([head, tail @ ..]) are the closest Rust equivalent to JavaScript’s array destructuring with rest (const [head, ...tail] = arr), but they also let you match on length — something JavaScript destructuring cannot express.
The other let-based pattern forms
Section titled “The other let-based pattern forms”match is the heavyweight, but four lighter forms cover the common cases:
fn main() { // 1. `if let` — match one pattern, ignore the rest. Like a `match` with // one interesting arm. Equivalent in spirit to TS `if (x !== undefined)`. let config: Option<u16> = Some(8080); if let Some(port) = config { println!("server on port {port}"); } else { println!("using default port"); }
// 2. `let ... else` — bind on the happy path, or diverge (return/break/panic). // Great for "early return on failure" without nesting. let raw = "127"; let Ok(n) = raw.parse::<i32>() else { println!("not a number"); return; }; println!("parsed {n}"); // n is in scope for the rest of the function
// 3. `while let` — loop as long as the pattern keeps matching. let mut stack = vec![1, 2, 3]; while let Some(top) = stack.pop() { println!("popped {top}"); }}Output (real):
server on port 8080parsed 127popped 3popped 2popped 1The fourth form is the matches! macro, which returns a bool for “does this value match this pattern?” — handy in conditions and .filter() closures:
#[allow(dead_code)] // `Click` is never constructed in this isolated snippet#[derive(Debug)]enum Event { Click { x: i64, y: i64 }, Close }
fn main() { let e = Event::Close; println!("is close? {}", matches!(e, Event::Close)); // is close? true}Exhaustiveness: the headline feature
Section titled “Exhaustiveness: the headline feature”A match must cover every possible value of the type. This is enforced structurally, at compile time, for every match — independent of whether the match returns a value or what that value’s type is. If you add a variant to an enum later, every match that does not handle it stops compiling — the compiler hands you a to-do list of every place you need to update. TypeScript does catch missing cases in a value-returning switch (via return-type flow analysis, as shown earlier), but that safety net disappears for side-effect (void) switches unless you hand-write a never-assignment assert. Rust needs no such opt-in: exhaustiveness is part of what match is.
Key Differences
Section titled “Key Differences”| Concept | TypeScript/JavaScript | Rust |
|---|---|---|
| Branching construct | switch statement (with fall-through) | match expression (no fall-through) |
| Returns a value | No (statement) | Yes (expression) |
| Destructuring | Separate from branching | Same mechanism as branching |
| Exhaustiveness | Automatic for a value-returning switch (return-type flow analysis); needs a never assert for void switches | Mandatory and structural for every match |
| Match on ranges | Manual if (x >= a && x <= b) | a..=b pattern |
| Match on multiple values | case a: case b: fall-through | a | b or-pattern |
| Conditions in a branch | if inside the case body | Match guard if cond on the arm |
| Bind while testing | Not available | name @ pattern |
| Slice/array length match | Not expressible by destructuring | [a, b, c] slice patterns |
| Moves a matched value? | N/A (no ownership) | Depends — match by &value to borrow |
Why exhaustiveness instead of a silent default?
Section titled “Why exhaustiveness instead of a silent default?”Rust’s reasoning: a missing case is almost always a bug, and a bug the compiler can see is a bug it should refuse to ship. The _ wildcard is always available when “everything else” is genuinely intended — but you have to write it on purpose, making the choice explicit rather than accidental. TypeScript gets you partway there: a value-returning switch with a known return type is checked, but a void switch is not unless you opt in with the never trick. Rust closes that gap — exhaustiveness applies to every match regardless of context, so you opt out (with _) rather than opt in to safety.
Patterns are refutable or irrefutable
Section titled “Patterns are refutable or irrefutable”A pattern that can fail to match is refutable (Some(x)); one that always matches is irrefutable ((a, b), Point { x, y }). let and function parameters require irrefutable patterns — let Some(x) = opt; is a compile error because it might fail. That is exactly why if let, let else, and while let exist: they are the constructs that allow refutable patterns by giving a fallback path.
Common Pitfalls
Section titled “Common Pitfalls”Pitfall 1: Forgetting a case (which is the point)
Section titled “Pitfall 1: Forgetting a case (which is the point)”enum Direction { North, South, East, West }
fn label(d: Direction) -> &'static str { // does not compile (error[E0004]: non-exhaustive patterns: `Direction::West` not covered) match d { Direction::North => "up", Direction::South => "down", Direction::East => "right", // forgot West! }}The real compiler error:
error[E0004]: non-exhaustive patterns: `Direction::West` not covered --> src/main.rs:4:11 |4 | match d { | ^ pattern `Direction::West` not covered |note: `Direction` defined here --> src/main.rs:1:6 |1 | enum Direction { North, South, East, West } | ^^^^^^^^^ ---- not covered = note: the matched value is of type `Direction`help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown |7 ~ Direction::East => "right",8 ~ Direction::West => todo!(), |This is not a problem to “fix and forget” — it is the feature working. Add the missing arm. Reach for _ => ... only when you truly want a catch-all, and prefer naming variants explicitly so that adding a variant later forces you to revisit the match.
Pitfall 2: A catch-all arm placed too early
Section titled “Pitfall 2: A catch-all arm placed too early”Arms are tried top to bottom, so a _ (or any irrefutable pattern) before more specific arms makes those later arms dead code.
fn describe(n: i32) -> &'static str { match n { _ => "anything", 0 => "zero", // unreachable }}The real compiler warning (this compiles, but warns):
warning: unreachable pattern --> src/main.rs:4:9 |3 | _ => "anything", | - matches any value4 | 0 => "zero", | ^ no value can reach this | = note: `#[warn(unreachable_patterns)]` on by defaultPut specific patterns first and the wildcard last.
Pitfall 3: Matching against a variable instead of comparing to it
Section titled “Pitfall 3: Matching against a variable instead of comparing to it”This is the trap that surprises every TypeScript developer. In a switch, case expected: compares against the value of expected. In a Rust match, a bare lowercase identifier is a new binding that matches anything — it does not compare against an existing variable.
fn main() { let expected = 3; let value = 7; match value { expected => println!("matched expected!"), // binds a NEW `expected`, always matches _ => println!("did not match"), }}The compiler warns loudly, and even explains the underlying rule:
warning: unreachable pattern --> src/main.rs:6:9 |5 | expected => println!("matched expected!"), // BUG: binds, doesn't compare | -------- matches any value6 | _ => println!("did not match"), | ^ no value can reach this |note: there is a binding of the same name; if you meant to pattern match against the value of that binding, that is a feature of constants that is not available for `let` bindings --> src/main.rs:2:9 |2 | let expected = 3; | ^^^^^^^^ = note: `#[warn(unreachable_patterns)]` on by defaultFix — use a match guard, or match against a const (which does compare, because const names in patterns are treated as values):
const EXPECTED: i32 = 3;
fn main() { let expected = 3; let value = 3;
// Option A: a guard compares against the variable. match value { v if v == expected => println!("guard matched"), _ => println!("no match"), }
// Option B: match against a const (UPPER_SNAKE_CASE) — this DOES compare. match value { EXPECTED => println!("const matched"), _ => println!("no match"), }}Output (real):
guard matchedconst matchedWarning: This is why Rust’s naming convention matters in patterns.
lowercase= a fresh binding (matches anything);UPPER_SNAKE_CASE(aconst) = a value comparison. The compiler relies on this casing distinction.
Pitfall 4: Using a refutable pattern in let
Section titled “Pitfall 4: Using a refutable pattern in let”fn main() { let opt: Option<i32> = Some(5); // does not compile (error[E0005]: refutable pattern in local binding) let Some(x) = opt; println!("{x}");}A plain let requires a pattern that always matches. Some(x) might be None, so it is refutable. Use if let, let ... else, or a full match instead:
fn main() { let opt: Option<i32> = Some(5); let Some(x) = opt else { println!("was None"); return; }; println!("{x}");}Best Practices
Section titled “Best Practices”- Prefer explicit variant arms over
_for enums you own. A wildcard silences the exhaustiveness check, so adding a variant later will not flag the match. Use_for open-ended primitive matches (status codes, characters) where listing everything is impossible. - Order arms specific-to-general. Put literals and tight ranges first, the wildcard last, to avoid unreachable-pattern warnings.
- Use
if let/let elsefor single-pattern checks, and reserve fullmatchfor genuine multi-way branching.let elsein particular flattens “parse-or-bail” logic that would otherwise nest deeply. - Match by reference (
match &value) when you do not need ownership, so the matched value remains usable afterward. Let match ergonomics infer the reference bindings rather than sprinklingref. - Reach for
matches!(value, Pattern)instead of amatchthat just returnstrue/false. - Use match guards for conditions that patterns cannot express (relationships between bindings,
x == some_var), but keep them simple — a guard is not a place for heavy logic. - Let the compiler be your refactoring tool. When you add an enum variant, do not add a
_arm to silence errors; let each brokenmatchguide you to every place that needs updating.
Real-World Example
Section titled “Real-World Example”A small HTTP-style request router driven entirely by pattern matching. It exercises tuple matching, struct and enum destructuring, reference bindings, guards, and an exhaustive response mapping — the kind of dispatch logic you would otherwise write as a tangle of if/else in Express.
// A tiny HTTP-style request router driven entirely by pattern matching.// Demonstrates: enum + struct destructuring, tuple matching, ref bindings,// guards, and exhaustiveness.
#[derive(Debug)]enum Method { Get, Post, Delete,}
#[derive(Debug)]struct Request { method: Method, path: String, body: Option<String>, // Some(body) for POST, None otherwise.}
#[derive(Debug)]enum Response { Ok(String), Created(String), NotFound, BadRequest(String),}
fn route(req: &Request) -> Response { // Match on a tuple of (&method, path-as-&str, &body). `.as_str()` lets us // write string-literal patterns; matching by reference avoids moving `req`. match (&req.method, req.path.as_str(), &req.body) { (Method::Get, "/health", _) => Response::Ok("healthy".into()),
// Path parameter extracted by hand, validated with a guard. (Method::Get, path, _) if path.starts_with("/users/") => { match path.trim_start_matches("/users/").parse::<u32>() { Ok(id) => Response::Ok(format!("user #{id}")), Err(_) => Response::BadRequest("invalid user id".into()), } }
// Create requires a body; bind it with `Some(b)`. (Method::Post, "/users", Some(b)) => Response::Created(format!("created: {b}")), (Method::Post, "/users", None) => Response::BadRequest("missing body".into()),
(Method::Delete, path, _) if path.starts_with("/users/") => { Response::Ok(format!("deleted {path}")) }
// Catch-all: every other (method, path) combination. _ => Response::NotFound, }}
fn status_line(res: &Response) -> String { // Exhaustive: every Response variant is mapped to a status line. match res { Response::Ok(msg) => format!("200 OK: {msg}"), Response::Created(msg) => format!("201 Created: {msg}"), Response::NotFound => "404 Not Found".to_string(), Response::BadRequest(why) => format!("400 Bad Request: {why}"), }}
fn main() { let requests = [ Request { method: Method::Get, path: "/health".into(), body: None }, Request { method: Method::Get, path: "/users/42".into(), body: None }, Request { method: Method::Get, path: "/users/abc".into(), body: None }, Request { method: Method::Post, path: "/users".into(), body: Some("Ada".into()) }, Request { method: Method::Post, path: "/users".into(), body: None }, Request { method: Method::Delete, path: "/users/7".into(), body: None }, Request { method: Method::Get, path: "/unknown".into(), body: None }, ];
for req in &requests { let res = route(req); println!( "{:<6} {:<12} -> {}", format!("{:?}", req.method), req.path, status_line(&res) ); }}Output (real, from cargo run):
Get /health -> 200 OK: healthyGet /users/42 -> 200 OK: user #42Get /users/abc -> 400 Bad Request: invalid user idPost /users -> 201 Created: created: AdaPost /users -> 400 Bad Request: missing bodyDelete /users/7 -> 200 OK: deleted /users/7Get /unknown -> 404 Not FoundThe router compiles cleanly under cargo clippy with no warnings. Notice how matching the (method, path, body) tuple collapses what would be a nested if/switch mess into one flat, exhaustive decision table — and the status_line match guarantees you can never add a Response variant without giving it a status line.
Further Reading
Section titled “Further Reading”Official Documentation
Section titled “Official Documentation”- The Rust Book — The
matchControl Flow Construct - The Rust Book — Concise Control Flow with
if letandlet ... else - The Rust Book — Patterns and Matching (full chapter)
- Rust Reference — Patterns
- Rust by Example — Flow of Control:
match std::matches!macro
Related Topics in This Guide
Section titled “Related Topics in This Guide”- Enums — the data-carrying types you match on most often.
- Option Enum —
Some/Nonepatterns and combinators that often replace amatch. - Structs — defining the struct shapes you destructure here.
- Tuple Structs — matching tuple-struct and newtype patterns.
- impl Blocks — where
&selfmatching commonly lives. - Basic Types — tuples and the unit type, introduced earlier.
- Ownership — why matching by reference vs. by value matters.
- Collections —
while letwith iterators and slice patterns overVec.
Exercises
Section titled “Exercises”Exercise 1: Traffic Light Action
Section titled “Exercise 1: Traffic Light Action”Difficulty: Easy
Objective: Write an exhaustive match over an enum.
Instructions: Given the TrafficLight enum below, implement action so that Red returns "stop", Yellow returns "slow down", and Green returns "go". Do not use a _ wildcard — handle each variant explicitly so a future variant would force you to revisit the match.
#[derive(Debug)]enum TrafficLight { Red, Yellow, Green,}
fn action(light: &TrafficLight) -> &'static str { // TODO: match on every variant /* ??? */}
fn main() { for l in [TrafficLight::Red, TrafficLight::Yellow, TrafficLight::Green] { println!("{l:?} -> {}", action(&l)); }}Solution
#[derive(Debug)]enum TrafficLight { Red, Yellow, Green,}
fn action(light: &TrafficLight) -> &'static str { match light { TrafficLight::Red => "stop", TrafficLight::Yellow => "slow down", TrafficLight::Green => "go", }}
fn main() { for l in [TrafficLight::Red, TrafficLight::Yellow, TrafficLight::Green] { println!("{l:?} -> {}", action(&l)); }}Output:
Red -> stopYellow -> slow downGreen -> goExercise 2: Classify a Coordinate
Section titled “Exercise 2: Classify a Coordinate”Difficulty: Medium
Objective: Combine struct patterns, literal patterns, and match guards.
Instructions: Implement quadrant for the Coord struct. Return "origin" for (0, 0), "on y-axis" when x == 0, "on x-axis" when y == 0, and "quadrant I"/"II"/"III"/"IV" for the four quadrants. Match by reference and use guards for the quadrant comparisons.
#[derive(Debug)]struct Coord { x: i32, y: i32,}
fn quadrant(c: &Coord) -> &'static str { // TODO /* ??? */}
fn main() { for c in [ Coord { x: 0, y: 0 }, Coord { x: 0, y: 5 }, Coord { x: 5, y: 0 }, Coord { x: 2, y: 3 }, Coord { x: -2, y: 3 }, Coord { x: -2, y: -3 }, Coord { x: 2, y: -3 }, ] { println!("{c:?} -> {}", quadrant(&c)); }}Solution
#[derive(Debug)]struct Coord { x: i32, y: i32,}
fn quadrant(c: &Coord) -> &'static str { match c { Coord { x: 0, y: 0 } => "origin", Coord { x: 0, .. } => "on y-axis", Coord { y: 0, .. } => "on x-axis", // When matching `&Coord`, the bindings are `&i32`, so deref in guards. Coord { x, y } if *x > 0 && *y > 0 => "quadrant I", Coord { x, y } if *x < 0 && *y > 0 => "quadrant II", Coord { x, y } if *x < 0 && *y < 0 => "quadrant III", _ => "quadrant IV", }}
fn main() { for c in [ Coord { x: 0, y: 0 }, Coord { x: 0, y: 5 }, Coord { x: 5, y: 0 }, Coord { x: 2, y: 3 }, Coord { x: -2, y: 3 }, Coord { x: -2, y: -3 }, Coord { x: 2, y: -3 }, ] { println!("{c:?} -> {}", quadrant(&c)); }}Output:
Coord { x: 0, y: 0 } -> originCoord { x: 0, y: 5 } -> on y-axisCoord { x: 5, y: 0 } -> on x-axisCoord { x: 2, y: 3 } -> quadrant ICoord { x: -2, y: 3 } -> quadrant IICoord { x: -2, y: -3 } -> quadrant IIICoord { x: 2, y: -3 } -> quadrant IVExercise 3: Summarize a JSON-like Value
Section titled “Exercise 3: Summarize a JSON-like Value”Difficulty: Hard
Objective: Match a recursive enum with data-carrying variants, including a guard that distinguishes an empty array.
Instructions: Given the Json enum, implement summarize so that Null -> "null", Bool(b) -> "bool(true)"/"bool(false)", Number(n) -> "number(3.5)", Text(s) -> "text(N chars)" using the string’s length, an empty Array -> "empty array", and a non-empty Array -> "array of N". Match the values by reference.
#[derive(Debug)]enum Json { Null, Bool(bool), Number(f64), Text(String), Array(Vec<Json>),}
fn summarize(value: &Json) -> String { // TODO /* ??? */}
fn main() { let doc = Json::Array(vec![ Json::Null, Json::Bool(true), Json::Number(3.5), Json::Text("hi".into()), Json::Array(vec![]), ]); if let Json::Array(items) = &doc { for item in items { println!("{}", summarize(item)); } } println!("top: {}", summarize(&doc));}Solution
#[derive(Debug)]enum Json { Null, Bool(bool), Number(f64), Text(String), Array(Vec<Json>),}
fn summarize(value: &Json) -> String { match value { Json::Null => "null".to_string(), Json::Bool(b) => format!("bool({b})"), Json::Number(n) => format!("number({n})"), Json::Text(s) => format!("text({} chars)", s.len()), // A guard distinguishes the empty case before the general one. Json::Array(items) if items.is_empty() => "empty array".to_string(), Json::Array(items) => format!("array of {}", items.len()), }}
fn main() { let doc = Json::Array(vec![ Json::Null, Json::Bool(true), Json::Number(3.5), Json::Text("hi".into()), Json::Array(vec![]), ]); if let Json::Array(items) = &doc { for item in items { println!("{}", summarize(item)); } } println!("top: {}", summarize(&doc));}Output:
nullbool(true)number(3.5)text(2 chars)empty arraytop: array of 5Tip: The ordering of the two
Json::Arrayarms matters: the guardedis_empty()arm must come first, because the unguardedJson::Array(items)arm would otherwise match every array and make the empty case unreachable.