# Learning Rust #3 — Enums, Pattern Matching, Traits, and Generics
Table of Contents
As an experienced developer, I’ve learned that types are your first line of defense. In Rust, enum + pattern matching lets you model reality precisely, and traits + generics give you reusable, zero‑overhead abstractions.
Goals for Part 3
- Model a domain with enums and destructuring
match - Lean on
Option/Resultand the?operator - Understand traits (interfaces) and when to use generics vs trait objects
- Write iterator‑friendly, allocation‑light code without sacrificing readability
Domain modeling with enums
Let’s sketch a tiny command protocol:
#[derive(Debug, Clone, PartialEq)]pub enum Command { Add { a: i64, b: i64 }, Concat(String, String), Quit,}
pub fn execute(cmd: Command) -> Option<String> { match cmd { Command::Add { a, b } => Some((a + b).to_string()), Command::Concat(a, b) => Some(format!("{a}{b}")), Command::Quit => None, }}Notes:
- Variants can be struct‑like (
Add { a, b }) or tuple‑like (Concat(...)). matchis exhaustive: add a new variant and the compiler shows you every place that must handle it.
Pattern guards and wildcards
fn grade(score: u8) -> &'static str { match score { s if s >= 90 => "A", 80..=89 => "B", 70..=79 => "C", _ => "D", }}- Use
_for a catch‑all, but prefer explicit ranges to avoid masking logic errors.
if let and while let
let maybe = Some(42);if let Some(x) = maybe { println!("x={x}");}
let mut it = [Some(1), None, Some(2)].into_iter();while let Some(Some(v)) = it.next() { println!("{v}");}Option, Result, and the ? operator
Ergonomic error handling with no exceptions.
use std::fs;use std::io;
fn read_first_line(path: &str) -> Result<String, io::Error> { let content = fs::read_to_string(path)?; // bubbles up io::Error Ok(content.lines().next().unwrap_or("").to_string())}Mapping and chaining:
fn parse_int(s: &str) -> Result<i64, String> { s.trim().parse::<i64>().map_err(|e| e.to_string())}
fn add_pair(a: &str, b: &str) -> Result<i64, String> { let x = parse_int(a)?; let y = parse_int(b)?; Ok(x + y)}Option helpers avoid branching:
fn first_nonempty<'a>(items: impl IntoIterator<Item = &'a str>) -> Option<&'a str> { items.into_iter().find(|s| !s.trim().is_empty())}Traits and generics
A trait is an interface. Implement it for any type.
pub trait Summarize { fn summary(&self) -> String; fn short(&self) -> String { self.summary() } // default method}
impl Summarize for i64 { fn summary(&self) -> String { format!("int({self})") }}
impl Summarize for String { fn summary(&self) -> String { format!("str(len={})", self.len()) }}Generic functions (static dispatch)
pub fn print_all<T: Summarize>(items: &[T]) { for it in items { println!("{}", it.summary()); }}- Monomorphization: the compiler generates specialized versions per concrete
T. No virtual calls. - Great for performance‑sensitive, hot‑path code.
Trait objects (dynamic dispatch)
pub fn print_dyn(items: &[&dyn Summarize]) { for it in items { println!("{}", it.summary()); }}- Use when you need heterogeneous collections or to reduce compile times across many generic instantiations.
- Costs: an extra indirection; generally negligible unless in tight loops.
impl Trait in arguments and returns
// Accept any iterator of &str without naming the concrete type.pub fn join_lines(lines: impl IntoIterator<Item = String>) -> String { lines.into_iter().collect::<Vec<_>>().join("\n")}impl Trait keeps signatures tidy while staying generic.
Iterators: expressive, lazy, zero‑alloc when you want
Refactor a manual loop into an iterator pipeline:
// total of squares of even numberslet v = vec![1, 2, 3, 4, 5, 6];let total: i32 = v.iter() .copied() // &i32 -> i32 .filter(|x| x % 2 == 0) .map(|x| x * x) .sum();When you need to avoid temporary allocations, prefer iterators and borrowed views over building intermediate Vecs.
Mini‑project: A typed, pattern‑matched calculator
We’ll parse commands like add 2 3 or concat hello world. No allocations for slices we can borrow.
#[derive(Debug, Clone, PartialEq)]pub enum Cmd<'a> { Add(i64, i64), Concat(&'a str, &'a str), Quit,}
pub fn parse<'a>(line: &'a str) -> Option<Cmd<'a>> { let mut it = line.split_whitespace(); let op = it.next()?; match op { "add" => { let a = it.next()?.parse().ok()?; let b = it.next()?.parse().ok()?; Some(Cmd::Add(a, b)) } "concat" => Some(Cmd::Concat(it.next()?, it.next()?)), "quit" => Some(Cmd::Quit), _ => None, }}
pub fn run(cmd: Cmd<'_>) -> Option<String> { match cmd { Cmd::Add(a, b) => Some((a + b).to_string()), Cmd::Concat(a, b) => Some(format!("{a}{b}")), Cmd::Quit => None, }}
#[cfg(test)]mod tests { use super::*;
#[test] fn parses_and_executes_add() { let c = parse("add 2 3").unwrap(); assert_eq!(run(c), Some("5".into())); }
#[test] fn parses_concat_borrowed() { let c = parse("concat hello world").unwrap(); assert_eq!(run(c), Some("helloworld".into())); }
#[test] fn quit_returns_none() { assert_eq!(run(parse("quit").unwrap()), None); }}Cmd<'a>borrows string slices from the input line; no extraStringallocations.- Pattern matching keeps control flow explicit and compiler‑checked.
Practical tips
- Prefer
enumover “stringly‑typed” flags. The compiler enforces exhaustiveness. - Reach for generics when call‑site types are known and hot; reach for trait objects for heterogeneous collections, plug‑in systems, or to decouple crates.
- Use iterator adaptors (
map,filter,flat_map) to express intent and avoid temporary buffers.
Exercises (15–25 minutes)
- Add a new command
mul <a> <b>to the calculator. Updateparseand make the compiler guide you to missing matches. - Create a trait
CommandExec { fn exec(&self) -> Option<String>; }and implement it forCmd<'_>. Compare generic vs dyn dispatch call sites. - Write
top_k<'a>(it: impl IntoIterator<Item = &'a str>, k: usize) -> Vec<&'a str>that returns theklongest strings without allocating intermediate owned strings. - Refactor
join_linesto takeimpl Iterator<Item = &'a str>and return aStringwhile minimizing allocations (hint: precompute capacity).
What clicked today
- Enums +
matchturn fuzzy inputs into typed states the compiler can reason about. ResultandOptioninteroperate cleanly with iterator chains; the?operator keeps the happy path visible.- Generics are zero‑cost by default via monomorphization; trait objects trade a tiny runtime cost for flexibility. Use the right tool for each boundary.
Next up (Part 4): pragmatic error handling with Result, module organization that scales, and tests you’ll actually keep running.