Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

Learn RustChapter 5: Shapes

Lesson 5.5 · 13 min

Matching the shape of a value

Type /h to Chatterbox and it says it does not know that command. Everyone types the short form sooner or later. Fixing it takes one character, |, and it opens the door to everything else a match arm can say.

Adds to ChatterboxShort aliases like /h and /q, and /debug to see how a line was understood.

Compiler errors you will meet E0004

One arm, several words

Every arm of a match starts with a pattern: a description of a shape that a value may or may not have. "/help" is a pattern that fits exactly one piece of text. Command::Say(text) is a pattern that fits any Say and gives its contents a name. Rust checks the arms from top to bottom and runs the first one whose pattern fits.

The bar | joins patterns with "or": "/help" | "/h" fits either word. Both lead to the same code, so the answer is written once and the two words can never drift apart.

PredictShort forms
fn meaning(word: &str) -> String {
match word {
"/help" | "/h" => String::from("help"),
"/quit" | "/q" | "/exit" => String::from("quit"),
_ => String::from("unknown"),
}
}
 
fn main() {
println!("{} {} {}", meaning("/h"), meaning("/exit"), meaning("/hq"));
}

What does this program print?

  • help quit unknown
  • help quit help
  • help unknown unknown
Reveal the answer

The program prints: help quit unknown Each side of | is a whole pattern. "/h" fits the first arm and "/exit" the second. "/hq" is neither "/h" nor "/q": a text pattern fits only exactly that text, so it falls to _.

Ranges and guards

A pattern can also be a range. 1..=9 fits any number from 1 to 9, both ends included (the = is what includes the 9). It works for numbers and for single characters, such as 'a'..='z' (single quotes hold exactly one character; double quotes hold text).

When the shape is not enough, add a guard: if and a question after the pattern. n if n >= 100 fits any number, names it n, and then only counts as a fit when the question is true. If the guard says no, Rust moves on to the next arm as if this one had not fitted at all.

PredictHow chatty?
fn chattiness(count: u32) {
match count {
0 => println!("not a word yet"),
1..=9 => println!("warming up"),
n if n >= 100 => println!("a whole novel"),
_ => println!("chatty"),
}
}
 
fn main() {
chattiness(10);
}

What does this program print?

  • warming up
  • chatty
  • a whole novel
Reveal the answer

The program prints: chatty 10 is not 0 and not in 1..=9, which stops at 9. The third pattern, n, fits any number, but its guard asks whether 10 is at least 100; it is not, so that arm is skipped and _ takes it.

Taking a value apart

A pattern can be shaped like a struct, a pair or a variant, and then it does more than check: it takes the value apart and gives each piece its own name. This works in a let too, not only in a match. Watch what happens to the keys when a Message is taken apart.

Ownership Stagelet Message { author, text } = m moves both fields out

Step through the program. Solid arrows are ownership; dashed arrows are borrows. Use ← → or the slider.

struct Message {
author: String,
text: String,
}
 
fn main() {
let m = Message {
author: String::from("you"),
text: String::from("hi"),
};
let Message { author, text } = m;
println!("{author} said {text}");
}
  1. Line 6. The program starts in main.
  2. Line 7. A Message is built. Its two fields each own a String in the heap: m.author owns "you" and m.text owns "hi".
  3. Line 11. The pattern has the shape of a Message, with a name where each field goes. The author key moves to a new owner called author. No text is copied.
  4. Line 11. The same line moves the text key to text. Both fields have moved out, so m can no longer be used as a whole.
  5. Line 12. The two new names are ordinary owners and print like any String. stdout: you said hi
  6. Line 13. main ends. author and text are dropped, and their heap blocks are freed.

Taking apart is moving, by the rules of chapter 4: each field's key goes to its new name. When you only want to look, take apart a borrow instead, as Chatterbox does with &last: then the names inside are borrows and the original stays whole. Taking a pair apart is the same idea: let (major, minor) = VERSION; gives the two numbers of lesson 2.6 a name each.

Two marks say "I do not need this part". _ skips exactly one piece and binds nothing, so nothing moves: Command::Say(_) fits any Say without touching its text. .. skips all the rest: Message { author, .. } takes only the author.

if let and while let

Sometimes only one shape matters. if let Some(text) = &last { … } else { … } is a match with one interesting arm: run the block if the value fits, with text named inside; otherwise run the else. while let is the same question asked again and again, and the loop stops the first time the value does not fit. Here it peels a sentence apart one word at a time:

while let: keep going while there is still a space
fn main() {
let mut rest = "rust is fun";
let mut words = 1;
while let Some(i) = rest.find(' ') {
println!("peeled off: {}", &rest[..i]);
rest = &rest[i + 1..];
words += 1;
}
println!("last word: {rest}, {words} words");
}
$ cargo run
peeled off: rust
peeled off: is
last word: fun, 3 words

Every shape, or the compiler says so

You met E0004 in lessons 3.2 and 5.3: a match has to cover every value. Patterns that look inside a value make that check sharper, because covering a variant now means covering every shape inside it too. Here Say carries an Option, and only one of its two shapes has an arm:

Compiler CoachThe compiler, translated

This is the exact output of the real compiler. Toggle the English, then pick a fix and see what actually happens.

enum Command {
Help,
Say(Option<String>),
}
 
fn answer(cmd: Command) -> String {
match cmd {
Command::Help => String::from("commands: /help, /say <text>"),
Command::Say(Some(text)) => text,
}
}
 
fn main() {
println!("{}", answer(Command::Say(None)));
}
error[E0004]: non-exhaustive patterns: `Command::Say(None)` not covered
  --> src/main.rs:7:11
   |
 7 |     match cmd {
   |           ^^^ pattern `Command::Say(None)` not covered
   |
note: `Command` defined here
  --> src/main.rs:1:6
   |
 1 | enum Command {
   |      ^^^^^^^
 2 |     Help,
 3 |     Say(Option<String>),
   |     --- not covered
   = note: the matched value is of type `Command`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
   |
 9 ~         Command::Say(Some(text)) => text,
10 ~         Command::Say(None) => todo!(),
   |

E0004 in the compiler error atlas →

  1. non-exhaustive patterns: `Command::Say(None)` not covered: there is an arm for a Say with text in it, and none for a Say with nothing. The compiler names the exact shape you forgot.
  2. The pointer under match cmd shows which match is incomplete. In a big program that matters: it is the one that will refuse to build.
  3. The note about Command shows where the enum is defined, so you can see every variant it could be.
  4. help: ensure that all possible cases are being handled: the compiler even writes the missing arm for you to fill in.

Fixes that were compiled for real

  • Add the missing shape — compiles. One more arm for a bare Say. Every shape now has its own answer, and if someone adds a variant later, this match will tell them.
  • Trap: join the two shapes with | — still E0408. An arm runs the same code for every pattern in it, so every pattern must give the same names. A bare Say has no text to call <code>text</code>, and the compiler refuses.
  • Catch everything else with _ — compiles, with a cost. It compiles, and today it even behaves. The cost comes later: when a new variant is added, <code>_</code> swallows it silently, and the compiler can no longer point you at this match.

Seeing how a line was understood

When Chatterbox gives a strange answer, the first question is "what did it think I typed?". That means printing a Command. But {} in println! only knows how to print things that have one obvious look for people, like text and numbers. A type you made has no such look until you write one, and asking for it is an error:

A type of your own does not know how to print itself
enum Command {
Help,
Say(Option<String>),
}
 
fn main() {
let cmd = Command::Say(Some(String::from("hi")));
println!("{:?}", cmd);
}
error[E0277]: `Command` doesn't implement `Debug`
 --> src/main.rs:8:22
  |
8 |     println!("{:?}", cmd);
  |               ----   ^^^ `Command` cannot be formatted using `{:?}` because it doesn't implement `Debug`
  |               |
  |               required by this formatting parameter
  |
  = help: the trait `Debug` is not implemented for `Command`
help: consider annotating `Command` with `#[derive(Debug)]`
  |
1 + #[derive(Debug)]
2 | enum Command {
  |

The {:?} asks for the debug form: a plain view of a value's shape, meant for the programmer. Command has none, so the compiler says it "doesn't implement Debug". You do not have to write that printing code by hand. Put #[derive(Debug)] on the line above the enum, and the compiler writes it for you: variant name, then whatever is inside, in brackets. The same works for structs. With it, that program prints Say(Some("hi")).

Chatterbox v0.5: short forms and /debug

Here is the whole program after this lesson, new lines in green.

  • In parse, two arms grow alternatives: "/help" | "/h" means help, and "/quit" | "/q" | "/exit" means quit. The help text lists them too: it now ends /quit (or /h, /q, /exit), after a new /debug <text>.
  • Command gets #[derive(Debug)] and a new variant, Debug(Option<String>), built with arg and owned exactly like Say.
  • In main, Command::Debug(Some(text)) parses text as if you had typed it, and prints the result with {:?}. So /debug /say hi shows Say(Some("hi")), and /debug hello shows Chat("hello"). (That first output will change in chapter 7, when parse learns to report errors: it becomes Ok(Say("hi")).) A bare /debug gets its own arm, so E0004 stays quiet.
  • /again is now an if let Some(text) = &last with an else. It does exactly what the match did; the shape you care about simply comes first.
Chatterbox v0.5 after lesson 5.5: the whole program (green lines are new in this lesson)
You type
Sam
/h
/q
after the quit
Chatterbox prints (a real run)
Chatterbox v0.5
What should I call you?
Hi Sam! I'm Chatterbox. Ask me anything.
bot: commands: /help, /again, /history, /say <text>, /shout <text>, /debug <text>, /quit (or /h, /q, /exit)
bot: bye, Sam!
messages so far: 0
Show the whole program (open it after you have tried the exercise below: it contains the answer)
src/main.rs15 new lines
// Chatterbox: a small chat program, built one lesson at a time.
use std::io;
 
/// What the bot is called. A constant: fixed for the whole program.
const BOT: &str = "Chatterbox";
/// The version as a pair of numbers: (major, minor).
const VERSION: (u32, u32) = (0, 5);
/// The longest message we accept, in bytes.
const MAX_LEN: usize = 200;
/// What /help prints.
const HELP: &str = "commands: /help, /again, /history, /say <text>, /shout <text>, /debug <text>, /quit (or /h, /q, /exit)";
 
/// One line of the conversation: who said it, and what they said.
struct Message {
author: String,
text: String,
}
 
impl Message {
/// Make a Message from two pieces of borrowed text.
fn new(author: &str, text: &str) -> Message {
Message {
author: String::from(author),
text: String::from(text),
}
}
 
/// The message as one transcript line: "you: hello".
fn to_line(&self) -> String {
format!("{}: {}", self.author, self.text)
}
 
/// The text in capitals, for /shout.
fn shout(&self) -> String {
self.text.to_uppercase()
}
}
 
/// Add one message to the transcript.
fn record(log: &mut String, msg: &Message) {
log.push_str(&msg.to_line());
log.push('\n');
}
 
/// Everything a line of input can mean. One enum, so a `match` on it must
/// handle every kind of line: the compiler checks that nothing is forgotten.
#[derive(Debug)]
enum Command {
Empty,
Help,
Quit,
Again,
History,
Say(Option<String>),
Shout(Option<String>),
Unknown(String),
Debug(Option<String>),
Chat(String),
}
 
/// A borrowed Option<&str> turned into an owned Option<String>, so the
/// Command can keep the text after `line` is gone.
fn owned(text: Option<&str>) -> Option<String> {
match text {
Some(t) => Some(String::from(t)),
None => None,
}
}
 
/// Turn one line of input into a Command.
fn parse(line: &str) -> Command {
match first_word(line) {
"" => Command::Empty,
"/help" | "/h" => Command::Help,
"/quit" | "/q" | "/exit" => Command::Quit,
"/again" => Command::Again,
"/history" => Command::History,
"/debug" => Command::Debug(owned(arg(line))),
"/say" => Command::Say(owned(arg(line))),
"/shout" => Command::Shout(owned(arg(line))),
word => {
if is_command(word) {
Command::Unknown(String::from(word))
} else {
Command::Chat(String::from(line))
}
}
}
}
 
/// The first line Chatterbox prints.
fn banner() -> String {
format!("{} v{}.{}", BOT, VERSION.0, VERSION.1)
}
 
/// The greeting, with a name filled in.
fn greeting(name: &str) -> String {
let mut text = format!("Hi {}! I'm {}.", name, BOT);
text.push_str(" Ask me anything.");
text
}
 
/// True when the text is a command: it starts with a slash.
fn is_command(text: &str) -> bool {
text.starts_with('/')
}
 
/// The first word of the text: a slice (&str) pointing into it, not a copy.
fn first_word(text: &str) -> &str {
match text.find(' ') {
Some(i) => &text[..i],
None => text,
}
}
 
/// The text after the command word, if there is any: Some("hello") for
/// "/say hello", None for a bare "/say". No more "" standing in for nothing.
fn arg(line: &str) -> Option<&str> {
match line.find(' ') {
Some(i) => {
let rest = line[i + 1..].trim();
if rest.is_empty() { None } else { Some(rest) }
}
None => None,
}
}
 
/// What the bot says back to an ordinary message (not a command).
fn reply(message: &str) -> String {
if message.len() > MAX_LEN {
format!("that is too long (the limit is {} bytes)", MAX_LEN)
} else if message.ends_with('?') {
String::from("Good question! I can only echo for now. Ask me again in chapter 9.")
} else {
format!("you said: {}", message)
}
}
 
fn main() {
let mut messages_seen: u32 = 0;
let quiet = false;
let mut last: Option<String> = None; // the last thing you said, for /again
let mut transcript = String::new(); // everything said so far
 
println!("{}", banner());
println!("What should I call you?");
let mut name_line = String::new();
io::stdin().read_line(&mut name_line).unwrap();
let name = name_line.trim();
if !quiet {
println!("{}", greeting(name));
}
 
let mut line = String::new();
loop {
line.clear();
let bytes = io::stdin().read_line(&mut line).unwrap();
if bytes == 0 {
break; // no more input: the keyboard was closed
}
let message = line.trim();
match parse(message) {
Command::Empty => {} // nothing typed: nothing to answer
Command::Quit => {
println!("bot: bye, {}!", name);
break;
}
Command::Help => println!("bot: {}", HELP),
Command::Again => {
if let Some(text) = &last {
println!("bot: {}", reply(text));
} else {
println!("bot: nothing to repeat yet");
}
}
Command::History => {
if transcript.is_empty() {
println!("bot: nothing said yet");
} else {
print!("{}", transcript);
}
}
Command::Say(Some(text)) => println!("bot: {}", text),
Command::Say(None) => println!("bot: say what? Try /say hello"),
Command::Shout(Some(text)) => println!("bot: {}", Message::new("you", &text).shout()),
Command::Shout(None) => println!("bot: shout what? Try /shout hello"),
Command::Unknown(word) => {
println!("bot: I don't know the command {}. Try /help.", word)
}
Command::Debug(Some(text)) => println!("bot: {:?}", parse(&text)),
Command::Debug(None) => println!("bot: debug what? Try /debug /say hi"),
Command::Chat(text) => {
messages_seen += 1;
let said = Message::new("you", &text);
let answer = Message::new("bot", &reply(&said.text));
record(&mut transcript, &said);
record(&mut transcript, &answer);
println!("bot: {}", answer.text);
last = Some(said.text); // the text moves out of `said`, which is not used again
}
}
}
println!("messages so far: {}", messages_seen);
}

Download this version (zip)Unzip, then cargo run inside the folder.

And here is /debug showing how two lines were understood. The quotes and brackets are the derived debug form, straight from the compiler's own printing code.

Chatterbox v0.5: a real run
You type
Sam
/debug /say hi
/debug hello
/quit
Chatterbox prints (a real run)
Chatterbox v0.5
What should I call you?
Hi Sam! I'm Chatterbox. Ask me anything.
bot: Say(Some("hi"))
bot: Chat("hello")
bot: bye, Sam!
messages so far: 0
Try itYour turn: teach parse the short forms

This is the whole Chatterbox program with the short forms taken out of parse: only /help and /quit work. Put them back, so that /h means help and both /q and /exit mean quit. A short form must be the whole word: /hq is still an unknown command.

This is the whole Chatterbox program with the short forms taken out of parse: only /help and /quit work. Put them back, so that /h means help and both /q and /exit mean quit. A short form must be the whole word: /hq is still an unknown command.

// Chatterbox: a small chat program, built one lesson at a time.
use std::io;
 
/// What the bot is called. A constant: fixed for the whole program.
const BOT: &str = "Chatterbox";
/// The version as a pair of numbers: (major, minor).
const VERSION: (u32, u32) = (0, 5);
/// The longest message we accept, in bytes.
const MAX_LEN: usize = 200;
/// What /help prints.
const HELP: &str = "commands: /help, /again, /history, /say <text>, /shout <text>, /debug <text>, /quit (or /h, /q, /exit)";
 
/// One line of the conversation: who said it, and what they said.
struct Message {
author: String,
text: String,
}
 
impl Message {
/// Make a Message from two pieces of borrowed text.
fn new(author: &str, text: &str) -> Message {
Message {
author: String::from(author),
text: String::from(text),
}
}
 
/// The message as one transcript line: "you: hello".
fn to_line(&self) -> String {
format!("{}: {}", self.author, self.text)
}
 
/// The text in capitals, for /shout.
fn shout(&self) -> String {
self.text.to_uppercase()
}
}
 
/// Add one message to the transcript.
fn record(log: &mut String, msg: &Message) {
log.push_str(&msg.to_line());
log.push('\n');
}
 
/// Everything a line of input can mean. One enum, so a `match` on it must
/// handle every kind of line: the compiler checks that nothing is forgotten.
#[derive(Debug)]
enum Command {
Empty,
Help,
Quit,
Again,
History,
Say(Option<String>),
Shout(Option<String>),
Unknown(String),
Debug(Option<String>),
Chat(String),
}
 
/// A borrowed Option<&str> turned into an owned Option<String>, so the
/// Command can keep the text after `line` is gone.
fn owned(text: Option<&str>) -> Option<String> {
match text {
Some(t) => Some(String::from(t)),
None => None,
}
}
 
/// Turn one line of input into a Command.
fn parse(line: &str) -> Command {
match first_word(line) {
"" => Command::Empty,
// short forms: /h means help; /q and /exit mean quit
"/help" => Command::Help,
"/quit" => Command::Quit,
"/again" => Command::Again,
"/history" => Command::History,
"/debug" => Command::Debug(owned(arg(line))),
"/say" => Command::Say(owned(arg(line))),
"/shout" => Command::Shout(owned(arg(line))),
word => {
if is_command(word) {
Command::Unknown(String::from(word))
} else {
Command::Chat(String::from(line))
}
}
}
}
 
/// The first line Chatterbox prints.
fn banner() -> String {
format!("{} v{}.{}", BOT, VERSION.0, VERSION.1)
}
 
/// The greeting, with a name filled in.
fn greeting(name: &str) -> String {
let mut text = format!("Hi {}! I'm {}.", name, BOT);
text.push_str(" Ask me anything.");
text
}
 
/// True when the text is a command: it starts with a slash.
fn is_command(text: &str) -> bool {
text.starts_with('/')
}
 
/// The first word of the text: a slice (&str) pointing into it, not a copy.
fn first_word(text: &str) -> &str {
match text.find(' ') {
Some(i) => &text[..i],
None => text,
}
}
 
/// The text after the command word, if there is any: Some("hello") for
/// "/say hello", None for a bare "/say". No more "" standing in for nothing.
fn arg(line: &str) -> Option<&str> {
match line.find(' ') {
Some(i) => {
let rest = line[i + 1..].trim();
if rest.is_empty() { None } else { Some(rest) }
}
None => None,
}
}
 
/// What the bot says back to an ordinary message (not a command).
fn reply(message: &str) -> String {
if message.len() > MAX_LEN {
format!("that is too long (the limit is {} bytes)", MAX_LEN)
} else if message.ends_with('?') {
String::from("Good question! I can only echo for now. Ask me again in chapter 9.")
} else {
format!("you said: {}", message)
}
}
 
fn main() {
let mut messages_seen: u32 = 0;
let quiet = false;
let mut last: Option<String> = None; // the last thing you said, for /again
let mut transcript = String::new(); // everything said so far
 
println!("{}", banner());
println!("What should I call you?");
let mut name_line = String::new();
io::stdin().read_line(&mut name_line).unwrap();
let name = name_line.trim();
if !quiet {
println!("{}", greeting(name));
}
 
let mut line = String::new();
loop {
line.clear();
let bytes = io::stdin().read_line(&mut line).unwrap();
if bytes == 0 {
break; // no more input: the keyboard was closed
}
let message = line.trim();
match parse(message) {
Command::Empty => {} // nothing typed: nothing to answer
Command::Quit => {
println!("bot: bye, {}!", name);
break;
}
Command::Help => println!("bot: {}", HELP),
Command::Again => {
if let Some(text) = &last {
println!("bot: {}", reply(text));
} else {
println!("bot: nothing to repeat yet");
}
}
Command::History => {
if transcript.is_empty() {
println!("bot: nothing said yet");
} else {
print!("{}", transcript);
}
}
Command::Say(Some(text)) => println!("bot: {}", text),
Command::Say(None) => println!("bot: say what? Try /say hello"),
Command::Shout(Some(text)) => println!("bot: {}", Message::new("you", &text).shout()),
Command::Shout(None) => println!("bot: shout what? Try /shout hello"),
Command::Unknown(word) => {
println!("bot: I don't know the command {}. Try /help.", word)
}
Command::Debug(Some(text)) => println!("bot: {:?}", parse(&text)),
Command::Debug(None) => println!("bot: debug what? Try /debug /say hi"),
Command::Chat(text) => {
messages_seen += 1;
let said = Message::new("you", &text);
let answer = Message::new("bot", &reply(&said.text));
record(&mut transcript, &said);
record(&mut transcript, &answer);
println!("bot: {}", answer.text);
last = Some(said.text); // the text moves out of `said`, which is not used again
}
}
}
println!("messages so far: {}", messages_seen);
}

Hidden tests (4): a_short_form_is_a_whole_word debug_shows_the_parsed_shape quit_and_both_short_forms help_and_its_short_form

This exercise runs real Rust in your browser through the official Rust Playground, so it needs JavaScript.

Self-check

Say it out loud, then reveal

Self-checkRetrieval beats re-reading
What does <code>"/quit" | "/q"</code> mean as a pattern?Reveal
Either text fits. The bar joins whole patterns with "or", and both lead to the same arm.
What happens when an arm's guard says no?Reveal
The arm counts as not fitting, and Rust tries the next arm down.
Why did joining <code>Say(Some(text)) | Say(None)</code> in one arm fail?Reveal
Every pattern in an arm must give the same names, and a bare Say has no text. That is E0408.
What does <code>#[derive(Debug)]</code> do?Reveal
It asks the compiler to write the code that prints a value in its debug form, so {:?} works on your own type.

That is lesson 5.5. Tick it off so the course remembers where you are.

Questions people ask

What is a pattern in Rust?

A pattern describes a shape a value may have, such as an exact piece of text, a range of numbers or an enum variant with something inside. A match runs the first arm whose pattern fits, and a pattern can give names to the parts it takes out.

How do I match several values in one Rust match arm?

Join them with a bar, as in "/quit" | "/q" | "/exit". Every pattern in the arm must give the same names.

What is the difference between _ and .. in a pattern?

An underscore skips exactly one piece and binds nothing. Two dots skip all the remaining pieces, for example every field of a struct you did not name.

What is the difference between if let and match?

if let checks a single pattern and runs its block only when the value fits, with an optional else. match lists arms for every possible shape, and the compiler checks that none is missing.

Why does println with {:?} fail on my own type?

Your type has no debug form yet. Add #[derive(Debug)] above the struct or enum, and the compiler writes the printing code for you.

Learn Rust by building a chat app · Lesson 5.5 · every compiler message above is real output from rustc 1.99.0 (b940084d7 2026-09-28) (edition 2024) · Vibe Engines · 2026