class: title # Enums and Pattern Matching ## Scott Rixner and Alan Cox --- layout: true --- ## What is an Enum? * In Rust, `enums` are not just integers; they are containers for data. * An `enum` is a "sum type" * Unlike a struct ("product type"), an enum represents a choice. * Struct: *this* AND *that*, enum: *this* OR *that*. * Memory layout * `Enums` are stored as a "tagged union". * Tag (discriminant) identifies which variant is active. * Payload is the actual data associated with that variant. * Compiler uses tags to ensure you never access the wrong memory. --- ## Basic Syntax ```rust enum LogLevel { Debug, Info, Warn, Error, } let level = LogLevel::Info; ``` * Each variant is a distinct value under the `LogLevel` namespace. * No data/payload associated with these variants. --- ## Enums with Associated Data Variants can hold different types and amounts of data. ```rust enum WebEvent { PageLoad, // No data KeyPress(char), // Single character Paste(String), // String Click { x: i64, y: i64 }, // Anonymous struct } let event = get_web_event(); match event { WebEvent::PageLoad => println!("Page loaded"), WebEvent::KeyPress(c) => println!("Key pressed: {}", c), WebEvent::Paste(s) => println!("Pasted: {}", s), WebEvent::Click { x, y } => println!("Clicked at: ({}, {})", x, y), } ``` --- ## Enums with Methods You can define methods on enums using `impl`, just like structs. ```rust impl WebEvent { fn inspect(&self) { match self { WebEvent::PageLoad => println!("Page loaded"), WebEvent::KeyPress(c) => println!("Key pressed: {}", c), WebEvent::Paste(s) => println!("Pasted: {}", s), WebEvent::Click { x, y } => println!("Clicked at: ({}, {})", x, y), } } } event.inspect(); ``` --- ## Memory Layout: Tagged Unions * `Enums` are stored as a **discriminant** (the tag) + the **payload**. * The size is determined by the largest payload variant. * The compiler ensures you only access the payload corresponding to the active tag. * Compiler must be able to determine payload size at compile time. --- ## The Cost of Enums: Padding `Enum` size = size of largest variant + tag size (padding added for alignment). ```rust enum Message { Quit, // 0 bytes + tag ChangeColor(i32, i32, i32), // 12 bytes + tag Write(String), // 24 bytes + tag Huge([u8; 1024]), // 1024 bytes + tag } ``` * Every `Message` instance will be ~1028 bytes, even `Message::Quit`! * **Implication:** Small variants waste space if one variant is huge. * **Solution:** Use `Box
` for large variants to store data on the heap. --- ## Recursive Enums * **Problem:** Rust needs to know the size of a type at compile time. * An enum cannot contain itself directly (infinite size). ```rust // Compile Error: recursive type has "infinite size" enum List { Cons(i32, List), Nil, } ``` * **Solution:** Indirection using pointers (`Box
`). ```rust enum List { Cons(i32, Box
), Nil, } ``` --- ## The Billion Dollar Mistake * In C/C++, `NULL` is a pointer that points to nothing. * Accessing it causes a segfault (at best) or a vulnerability (at worst). * Rust: eliminate null pointers entirely at the language level. --- ## Robust "null" Handling: `Option
` ```rust enum Option
{ Some(T), None, } ``` * If a value is optional, you **must** use the `Option` enum. * This is a generic enum: `T` can be any type. --- class: line-numbers ## Why Option Matters ```rust let x: i32 = 5; let ptr: &i32 = &x; let opt: Option<&i32> = Some(&x); // This works (dereferences the pointer): let sum = *ptr + 5; // This fails to compile: // No "null pointers" to accidentally dereference! // let sum = *opt + 5; ``` * Compiler does not allow you to use something that might not have a value. * You cannot accidentally dereference a null pointer! --- ## Option: Safe Extraction ```rust let n: Option
= Some(5); // 1. Pattern matching (match) let val = match n { Some(x) => x, None => 0, }; // 2. Safe extraction let val = n.unwrap_or_default(); // Use T's default value (0 for i32) let val = n.unwrap_or(0); // Use supplied default if None let val = n.unwrap_or_else(|| 0); // Compute default if None // 3. Panic if None (Use with caution!) let val = n.unwrap(); // Crashes program let val = n.expect("Required value missing"); // Crashes with message ``` * Must "unwrap" option or use match to access value. * Forces you to decide how to deal with the possibiity of `None`. --- ## Option: Null Pointer Optimization * For types that cannot be null (like `&T` or `Box
`), `Option<&T>` is the same size as a regular pointer. * Rust uses `0x0` to represent `None`. * **Implication:** You get safety for free with no memory overhead. --- ## Robust Error Handling: `Result
` ```rust enum Result
{ Ok(T), Err(E), } ``` * If an operation could fail, you should return the `Result` enum. * This is a generic enum: `T` and `E` can be any types. --- ## Result vs. C-Style Return Codes * C * Returns `-1`. * Did you check the return code? The compiler doesn't care. * Rust * Returns `Result`. * If you don't use it, the compiler issues a warning. * You **can't** use the `Ok` variant (actual result) if there was an error. * The `Err` variant can contain a full `Error` struct with context. --- ## The `?` Operator (Propagation) ```rust fn read_username() -> Result
{ let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s) } ``` * If any call returns `Err`, the entire function returns that `Err` immediately. * Note that the expression at the end of the function is the return value. --- ## Error Conversions * When using `?`, Rust can automatically convert error types if `From` is implemented. * This allows you to combine different types of errors in one function. --- ## Result: `.map()` and `.map_err()` ```rust let ok_val: Result
= Ok(10); let err_val: Result
= Err("error"); // .map() applies fn only to Ok, leaves Err alone let a = ok_val.map(|x| x * 2); // Ok(20) let b = err_val.map(|x| x * 2); // Err("error") // .map_err() applies fn only to Err, leaves Ok alone let c = ok_val.map_err(|s| s.len()); // Ok(10) let d = err_val.map_err(|s| s.len()); // Err(5) ``` * Transform the success (`map`) or error (`map_err`) value safely. * Useful for converting types without explicit matching. --- ## Functional Chaining: `.and_then()` ```rust fn get_id(name: &str) -> Result
{ ... } fn get_user(id: i32) -> Result
{ ... } fn get_home(user: User) -> Result
{ ... } // "Pipeline" style: input -> get_id -> get_user -> get_home let home = get_id("alice") .and_then(get_user) .and_then(get_home); // Equivalent to: // match get_id("alice") { // Ok(id) => match get_user(id) { // Ok(user) => get_home(user), // Err(e) => Err(e), // Error propagates immediately // }, // Err(e) => Err(e), // } ``` * Short-circuiting: If any step returns `None`/`Err`, the chain stops. * Flattening: Prevents nested types like `Option
>`. --- ## `Option` and `Result` Transformations .center[  ] --- ## Pattern Matching: The match Control Flow * `match` allows you to compare a value against a series of patterns. * It is **exhaustive**: every possible case must be covered. * For enums, `match` is compiled into a **jump table**. * The CPU can jump directly to the code for the correct variant. * Better performance for larger enums. --- ## Pattern Matching: Literals and Ranges ```rust let x = 1; match x { 1 => println!("one"), 2 | 3 => println!("two or three"), 4..=10 => println!("four through ten"), _ => println!("anything else"), } ``` --- ## Destructuring with match Pulling data out of nested structures. ```rust struct Point { x: i32, y: i32 } let p = Point { x: 0, y: 7 }; match p { Point { x, y: 0 } => println!("On the x axis at {x}"), Point { x: 0, y } => println!("On the y axis at {y}"), Point { x, y } => println!("At ({x}, {y})"), } ``` --- ## Ignoring Values Use `..` to ignore the rest of a struct or tuple. ```rust let s = WebEvent::Click { x: 10, y: 20 }; match s { WebEvent::Click { x, .. } => println!("x is {x}"), _ => (), } ``` --- ## Match Guards Add extra logic to a pattern. ```rust let num = Some(4); match num { Some(x) if x % 2 == 0 => println!("Even number"), Some(_) => println!("Odd number"), None => (), } ``` --- ## Concise Control Flow: `if let` ```rust let val = Some(42); // Verbose match match val { Some(x) => println!("Value: {}", x), _ => (), // Required to handle None } // Concise if let if let Some(x) = val { println!("Value: {}", x); } ``` * Less verbose than `match` when you only care about one case. * Reduces boilerplate (no need for `_ => ()`). * Loses exhaustiveness checking (compiler won't warn if you miss a case). --- ## Pattern Matching with `while let` Continue looping as long as a pattern matches. ```rust let mut stack = vec![1, 2, 3]; while let Some(top) = stack.pop() { println!("{top}"); } ``` --- ## Making Illegal States Unrepresentable * **C Approach:** Use a struct with a "type" field and a union. * **Rust Approach:** The compiler **guarantees** you only access the data associated with the current tag. --- ## Designing with Enums: Example ```rust // Instead of this: struct Connection { is_connected: bool, socket: Option
} // Do this: enum Connection { Disconnected, Connected(TcpStream) } ``` * If you add a `Encrypted(TcpStream)` variant to the enum above, the compiler will find every `match` statement in your codebase that needs updating. * This is valuable for large-scale systems programming. --- ## Summary 1. **Enums:** Define all of the possible states of your data. 2. **Option/Result:** Handle "nothing" and "failure" explicitly. 3. **Match:** Efficiently and safely handle all `enum` variants. 4. **Performance:** Tagged unions, null pointer optimization, and jump tables.