Step 7 of 7
Modeling with enums: parsing commands
Enums and Option together let you design types where bad data simply can't exist. Two patterns come up constantly at work.
States that carry their own data
Picture a network connection modeled with a struct:
struct Connection {
connected: bool,
connecting: bool,
attempt: u32, // only meaningful while connecting
session_id: u64, // only meaningful while connected
}
What does connected: true, connecting: true mean? What's the session id of a disconnected connection? Every function has to remember the unwritten rules. As an enum, each state carries exactly the data that makes sense for it, and impossible combinations can't be built:
enum Connection {
Disconnected,
Connecting { attempt: u32 },
Connected { session_id: u64 },
}
fn status(conn: &Connection) -> String {
match conn {
Connection::Disconnected => String::from("offline"),
Connection::Connecting { attempt } => format!("connecting (attempt {})", attempt),
Connection::Connected { session_id } => format!("online, session {}", session_id),
}
}
fn main() {
let states = [
Connection::Disconnected,
Connection::Connecting { attempt: 2 },
Connection::Connected { session_id: 9041 },
];
for s in &states {
println!("{}", status(s));
}
}
offline
connecting (attempt 2)
online, session 9041
Parse once, at the edge
Text from users, files and networks is messy. The robust approach is to turn it into an enum as soon as it enters the program, with a function that returns Option (or, later, Result). Everything after that works with clean, typed values: no more string comparisons scattered around, no "what if the number is missing?" deep inside the logic. People call this parse, don't validate.
enum ChatCommand {
Join(String),
Nick(String),
Quit,
}
fn parse(line: &str) -> Option<ChatCommand> {
let mut words = line.split_whitespace();
let Some(name) = words.next() else {
return None;
};
let command = match name {
"/join" => {
let Some(room) = words.next() else {
return None;
};
ChatCommand::Join(room.to_string())
}
"/nick" => {
let Some(nick) = words.next() else {
return None;
};
ChatCommand::Nick(nick.to_string())
}
"/quit" => ChatCommand::Quit,
_ => return None,
};
Some(command)
}
fn run(cmd: &ChatCommand) -> String {
match cmd {
ChatCommand::Join(room) => format!("joining #{}", room),
ChatCommand::Nick(nick) => format!("you are now {}", nick),
ChatCommand::Quit => String::from("bye"),
}
}
fn main() {
let lines = ["/join rust", "/nick ferris", "/dance", "/join", "/quit"];
for line in lines {
if let Some(cmd) = parse(line) {
println!("{}", run(&cmd));
} else {
println!("can't parse: {}", line);
}
}
}
joining #rust
you are now ferris
can't parse: /dance
can't parse: /join
bye
words.next()gives the words one at a time, asSome(word)orNonewhen they run out.let command = match name { ... };usesmatchas an expression. The_ => return Nonearm leaves the function early:returnnever produces a value, so Rust accepts it in an arm of any type.rundoesn't parse anything. By the time aChatCommandexists, it's guaranteed valid, and thematchguarantees every command is handled.
Your task
Write fn parse_command(line: &str) -> Option<Command> for a warehouse robot. Valid lines are:
forward Nwhere N is a whole number that isn't negative (au32):Command::Forward(N)left,rightandreport:Command::Left,Command::Right,Command::Report
Anything else is None: an unknown word, a missing or invalid number, an empty line, or extra words after a complete command (left now).
Challenge 2
Same technique for a bank: write fn parse_transaction(line: &str) -> Option<Transaction>. Valid lines are:
deposit Nandwithdraw N(N is au64)transfer N NAME:Transaction::Transfer { amount: N, to: NAME }balance
Anything else is None, including missing or invalid numbers, a transfer without a name, and extra words.
Challenge 3
Put it all together: a robot program that reads commands from standard input. The starter has Direction (with turning methods), Command and a working parse_command. Write a Robot struct (position and facing) with methods, and a main that runs each line:
- The robot starts at
0,0facingNorth. North is y + 1, East x + 1, South y - 1, West x - 1. forward Nmoves N steps;leftandrightturn.reportprints the position and direction like2,3 East({:?}prints a Direction's name).- A line that doesn't parse prints
invalid:followed by the line.
For example:
forward 3
right
forward 2
report
prints 2,3 East.