class: title # Traits and Generics ## Scott Rixner and Alan Cox --- layout: true --- ## What is a Trait? * A **trait** defines shared behavior in an abstract way. * It tells the compiler what a type can *do*, regardless of its internal structure. * This is Rust's primary mechanism for polymorphism. --- ## Language Comparisons * **Java interfaces:** Traits are similar but can be implemented for existing types (like `i32` or `String`) without inheritance. * **Go interfaces:** Go interfaces are implicit (structural typing). Rust traits are explicit (nominal typing). You must indicate `impl Trait for Type`. * **C++ abstract classes:** Traits are like abstract base classes with pure virtual functions, but they don't carry data. --- ## Defining a Trait Imagine we are writing a kernel that supports multiple Serial Port controllers. ```rust pub trait SerialPort { // Required methods fn send(&mut self, byte: u8); fn read(&self) -> Option
; // Default implementation fn send_buffer(&mut self, buf: &[u8]) { for b in buf { self.send(*b); } } } ``` --- ## Implementing a Trait ```rust struct Uart16550 { base_address: usize, } impl SerialPort for Uart16550 { fn send(&mut self, byte: u8) { // In real OS code, we'd write to a raw pointer here unsafe { let ptr = self.base_address as *mut u8; *ptr = byte; } } fn read(&self) -> Option
{ // Check line status register... Some(0) } } ``` --- ## Ad-hoc Implementation Unlike Java, you can implement traits for types you didn't define (mostly). ```rust // Useful for mocking hardware in tests impl SerialPort for Vec
{ fn send(&mut self, byte: u8) { self.push(byte); } fn read(&self) -> Option
{ self.pop() } } ``` --- ## The Orphan Rule * **Constraint:** To implement a trait for a type, **either** the trait **or** the type must be defined in your crate. * **Why?** Prevents coherence issues where two crates define conflicting implementations. ```rust // Allowed: We defined SerialPort impl SerialPort for String { ... } // Allowed: We defined MyStruct impl std::fmt::Display for MyStruct { ... } // FORBIDDEN: We defined neither! impl std::fmt::Display for String { ... } ``` --- class: middle ## Questions: Implications of the Orphan Rule How does the orphan rule affect software design in Rust? How does this compare to languages with structural typing? --- ## Supertraits * A trait can require types to also implement another trait. * This required trait is called a **supertrait**. ```rust trait Device { fn status(&self) -> Status; } // BlockDevice requires Device (supertrait) trait BlockDevice: Device { fn read_block(&mut self, lba: u64, buf: &mut [u8]); } ``` --- ## Generics * Generics allow code reuse across different types. * Similar to C++ templates or Java generics, but with different performance characteristics. ```rust struct Point
{ x: T, y: T, } let integer_pt = Point { x: 5, y: 10 }; let float_pt = Point { x: 1.0, y: 4.0 }; ``` --- ## Trait Bounds How do we write a function that works on any `SerialPort`? ```rust // "T must implement SerialPort" fn log_status
(device: &mut T) { device.send_buffer(b"System Initialized"); } ``` Equivalent in TypeScript: ```typescript function log_status
(device: T) { // ... } ```` --- ## `impl Trait` Syntax * For simple bounds, you can use `impl Trait` as a shorthand. * This effectively compiles to the generic version on the previous slide. ```rust // Function parameters fn log_status(device: &mut impl SerialPort) { device.send_buffer(b"System Initialized"); } // Return types (hides concrete type) fn get_device() -> impl SerialPort { Uart16550 { base_address: 0x3F8 } } ``` **Limitation:** * Return type can only be one actual type. * You cannot return `Uart16550` in one branch and `Vec
` in another. --- ## Traits as Arguments Traits allow functions to operate on types that behave in a certain way, rather than requiring a specific concrete type. ```rust // Accepts a reference to any type that implements SerialPort. // Uses static dispatch - compiler generates specialized code for each type. fn use_port(port: &mut impl SerialPort) { port.send(0xFF); } let mut real = Uart16550 { base_address: 0x3F8 }; let mut mock = Vec::new(); use_port(&mut real); // Compiles to specialized code for Uart16550 use_port(&mut mock); // Compiles to specialized code for Vec
``` --- class: middle ## Question: Cost of Generic Arguments What costs might using trait bounds (`impl Trait`) have compared to using concrete types as function parameters? --- ## Generic Struct Implementations ```rust struct Buffer
{ data: Vec
, } // Methods for ANY type T impl
Buffer
{ fn new() -> Self { Buffer { data: Vec::new() } } } // Methods only for types that can be Copy-ed impl
Buffer
{ fn duplicate(&self) -> Vec
{ self.data.clone() } } ``` --- ## Associated Types Used when a trait needs a specific type to work, but that type depends on the implementation. ```rust pub trait Iterator { type Item; // Placeholder fn next(&mut self) -> Option
; } impl Iterator for Uart16550 { type Item = u8; // Concrete type fn next(&mut self) -> Option
{ self.read() } } ``` --- ## Monomorphization (The C++ Model) * Like **C++** templates, Rust compiles a separate copy of the function for each concrete type used. * **Java** and **TypeScript** generics use "type erasure" (one version of the code). * **Go** generics use one generic function per representation shape, with a table of type information and operations for the specific type passed at runtime. * **Rust/C++:** * Pros: Static dispatch, inlining, zero runtime overhead. * Cons: Larger binary size (code bloat), slower compile times. --- ## Dynamic Dispatch: Trait Objects * What if you *do* need to return different types or store them in a heterogeneous collection? * Use **Trait Objects** (`dyn Trait`). * `dyn` means dynamic dispatch. ```rust let mut uart = Uart16550 { base_address: 0x3F8 }; let mut mock = Vec::new(); // A Trait Object can point to any SerialPort let mut current_port: &mut dyn SerialPort = &mut uart; if use_simulation { current_port = &mut mock; // Now points to the Vec! } current_port.send(0x42); ``` --- ## Storing Traits * To store a trait in a struct or enum, you must use a trait object. * `Box` is a smart pointer (we'll discuss more later). * This allows `Console` to hold a `Uart16550`, a `UsbSerial`, or a simulated port at runtime. ```rust struct Console { // Pointer to any serial port implementation. port: Box
, } impl Console { fn new(port: Box
) -> Self { Console { port } } } // A list of heterogeneous devices let devices: Vec
> = vec![ Box::new(Uart16550 { base_address: 0x3F8 }), Box::new(Vec::new()), // The "Mock" device ]; ``` --- ## Vtables and Dispatch * `Box
` is a "fat pointer" (2 words): 1. Pointer to the data (the instance). 2. Pointer to the **vtable** (table of method pointers for `SerialPort`). * This is like a Java object reference or Go interface value. --- class: middle ## Question: Trade-offs of Dynamic Dispatch When should you choose static dispatch (`impl Trait`) vs. dynamic dispatch (`dyn Trait`)? Why? --- ## Common Traits: `Clone` and `Copy` * `Clone`: Explicit duplication (`x.clone()`). Can be expensive (e.g., copying a heap string). * `Copy`: Implicit bitwise copy (like `int` in C/Java). * Only valid for types that are pure stack data (integers, bools, pointers). * Types with destructors (like `File` or `Vec`) cannot be `Copy`. ```rust #[derive(Copy, Clone)] struct Register(u32); // Safe to memcpy ``` --- ## Common Traits: `Drop` * Analogous to C++ destructors. * Called automatically when a value goes out of scope. * Used to release resources (memory, file handles, locks). ```rust struct FileHandle { fd: i32 } impl Drop for FileHandle { fn drop(&mut self) { // close(self.fd); println!("File closed."); } } ``` --- ## Common Traits: `From` and `Into` * Used for infallible type conversions. * The `?` operator automatically calls `from` to convert errors. ```rust enum DiskError { Io(std::io::Error), BadFormat, } impl From
for DiskError { fn from(err: std::io::Error) -> Self { DiskError::Io(err) } } fn load_boot_sector() -> Result
, DiskError> { // The '?' operator automatically converts io::Error -> DiskError let data = std::fs::read("/boot/sector.bin")?; Ok(data) } ``` --- class: middle ## Question: Error Handling Design How do the `From`/`Into` traits and the `?` operator change error handling patterns? What are the advantages and disadvantages of automatic error conversion vs. explicit error handling? --- ## Derivable Traits * Rust can generate implementations for common traits automatically using the `#[derive]` attribute. * This avoids boilerplate for standard behavior like comparison, cloning, and printing. ```rust #[derive(Debug, Clone, PartialEq, Eq)] struct IpAddress { octets: [u8; 4], } let ip1 = IpAddress { octets: [127, 0, 0, 1] }; let ip2 = ip1.clone(); // Clone is derived if ip1 == ip2 { // PartialEq is derived println!("Addresses match: {:?}", ip1); // Debug is derived } ``` --- ## Disambiguation If a type implements two traits that have methods with the same name, you must use fully qualified syntax to tell the compiler which one you mean. ```rust trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("Plane"); } } impl Wizard for Human { fn fly(&self) { println!("Broom"); } } let person = Human; // person.fly(); // Error: Ambiguous Pilot::fly(&person); // Prints "Plane" Wizard::fly(&person); // Prints "Broom" ``` --- class: middle ## Questions: Design Complexity and Clarity When does Rust's trait system become too complex? What are the signs of over-engineering with traits? In performance-critical code, how do you balance abstraction with predictable behavior? --- ## Summary * Rust provides polymorphism via **traits**. * Static Dispatch (`
`) is the default zero-cost abstraction, similar to C++ templates. * Dynamic Dispatch (`dyn Trait`) is available when needed, providing flexibility at the cost of a vtable lookup, similar to Java/Go interfaces. * Orphan Rule ensures coherence. * Standard traits (`Drop`, `Clone`, `From`) are idiomatic ways to integrate with the language.