# 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.

Github

Goals for Part 3

  • Model a domain with enums and destructuring match
  • Lean on Option/Result and 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(...)).
  • match is 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 numbers
let 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 extra String allocations.
  • Pattern matching keeps control flow explicit and compiler‑checked.

Practical tips

  • Prefer enum over “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)

  1. Add a new command mul <a> <b> to the calculator. Update parse and make the compiler guide you to missing matches.
  2. Create a trait CommandExec { fn exec(&self) -> Option<String>; } and implement it for Cmd<'_>. Compare generic vs dyn dispatch call sites.
  3. Write top_k<'a>(it: impl IntoIterator<Item = &'a str>, k: usize) -> Vec<&'a str> that returns the k longest strings without allocating intermediate owned strings.
  4. Refactor join_lines to take impl Iterator<Item = &'a str> and return a String while minimizing allocations (hint: precompute capacity).

What clicked today

  • Enums + match turn fuzzy inputs into typed states the compiler can reason about.
  • Result and Option interoperate 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.

Next: Learning Rust #4 — Errors, Modules, and Testing
My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


Learning Rust Series

Comments