Skip to content
portfolio/howtos/Learning Rust/

Chapter 10

Traits & Generics

Write code that works with many types while keeping full type safety. Traits define shared behavior; generics let you use it.

Defining and Implementing Traits

A trait defines a set of methods that a type can implement. If you're coming from other languages: traits are like interfaces in Java/Go, or protocols in Swift - but more powerful because you can implement them for types you didn't define.

/// Anything that can be serialized to HTTP wire format.
trait ToHttp {
    fn to_http(&self) -> Vec<u8>;
}

// Implement for our Response type
impl ToHttp for Response {
    fn to_http(&self) -> Vec<u8> {
        let mut out = format!(
            "HTTP/1.1 {} {}\r\nContent-Length: {}\r\n\r\n{}",
            self.status, self.reason, self.body.len(), self.body
        );
        out.into_bytes()
    }
}

// Implement for a simple string - anything can be "to_http"
impl ToHttp for &str {
    fn to_http(&self) -> Vec<u8> {
        let body = *self;
        format!(
            "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
            body.len(), body
        ).into_bytes()
    }
}

Now any function that takes impl ToHttp works with both Response and &str. The trait is the contract - "I can be serialized to HTTP" - and each type fulfills it differently.

Default Methods

Traits can provide default implementations. Implementors can override them or use the default:

trait Loggable {
    /// Required - implementors must provide this
    fn log_line(&self) -> String;

    /// Default - provided by the trait, can be overridden
    fn log(&self) {
        println!("[LOG] {}", self.log_line());
    }

    /// Default that builds on another method
    fn log_with_prefix(&self, prefix: &str) {
        println!("[{}] {}", prefix, self.log_line());
    }
}

struct Request {
    method: String,
    path: String,
}

impl Loggable for Request {
    fn log_line(&self) -> String {
        format!("{} {}", self.method, self.path)
    }
    // log() and log_with_prefix() use the defaults
}

let req = Request { method: "GET".into(), path: "/".into() };
req.log();                        // [LOG] GET /
req.log_with_prefix("REQUEST");   // [REQUEST] GET /
Note
You've already used traits extensively. Display, Debug, From, Read, Write - these are all traits from the standard library. Every #[derive(Debug)] generates a trait implementation. Every stream.read() call dispatches through the Read trait.

The Orphan Rule

There's one restriction: you can only implement a trait for a type if you own either the trait or the type (or both). You can't implement someone else's trait for someone else's type:

// ✓ Your trait for a foreign type
impl ToHttp for String { /* ... */ }

// ✓ A foreign trait for your type
impl std::fmt::Display for Response { /* ... */ }

// ✗ A foreign trait for a foreign type - compile error
// impl std::fmt::Display for Vec<u8> { /* ... */ }

This prevents conflicting implementations. If two crates could both implement Display for Vec<u8>, which one would the compiler pick?

Trait Bounds and where Clauses

A trait bound says "this generic type must implement a specific trait." There are three equivalent syntaxes:

// 1. impl Trait - simplest, good for one or two bounds
fn send_response(response: &impl ToHttp, stream: &mut TcpStream) {
    let bytes = response.to_http();
    stream.write_all(&bytes).unwrap();
}

// 2. Angle bracket syntax - when you need to name the type parameter
fn send_response<T: ToHttp>(response: &T, stream: &mut TcpStream) {
    let bytes = response.to_http();
    stream.write_all(&bytes).unwrap();
}

// 3. where clause - cleanest when there are many bounds
fn send_response<T>(response: &T, stream: &mut TcpStream)
where
    T: ToHttp,
{
    let bytes = response.to_http();
    stream.write_all(&bytes).unwrap();
}

All three produce identical compiled code. The where clause is preferred when bounds get complex:

// Multiple bounds - where clause keeps the signature readable
fn process<T, E>(item: T) -> Result<String, E>
where
    T: ToHttp + Loggable + Send,
    E: From<std::io::Error> + std::fmt::Debug,
{
    item.log();
    let bytes = item.to_http();
    Ok(String::from_utf8_lossy(&bytes).to_string())
}

The + syntax means "must implement both." So T: ToHttp + Loggable means "T must implement ToHttp AND Loggable."

Tip
Use impl Trait in function parameters for simple cases. Switch to a named type parameter (<T: Trait>) when you need to refer to the type in multiple places. Switch to where when the signature gets long.

Generics on Structs, Enums, and Functions

Generics let you write code once that works with many types. You've been using generics since Chapter 2 - Vec<T>, Option<T>, Result<T, E>, HashMap<K, V> are all generic types.

Let's write our own:

Generic Structs

/// A cache that stores values of any type.
struct Cache<V> {
    data: HashMap<String, V>,
    max_size: usize,
}

impl<V> Cache<V> {
    fn new(max_size: usize) -> Self {
        Cache {
            data: HashMap::new(),
            max_size,
        }
    }

    fn get(&self, key: &str) -> Option<&V> {
        self.data.get(key)
    }

    fn insert(&mut self, key: String, value: V) {
        if self.data.len() >= self.max_size {
            // Evict the first key (not great, but simple)
            if let Some(first) = self.data.keys().next().cloned() {
                self.data.remove(&first);
            }
        }
        self.data.insert(key, value);
    }
}

// Works with any type
let mut page_cache: Cache<String> = Cache::new(100);
page_cache.insert("/".to_string(), "<h1>Home</h1>".to_string());

let mut count_cache: Cache<u64> = Cache::new(1000);
count_cache.insert("/".to_string(), 42);

The impl<V> declares the type parameter for the implementation block. Every method gets access to V.

Generic Functions

/// Return the first element, if any.
fn first<T>(items: &[T]) -> Option<&T> {
    items.first()
}

/// Find an item matching a predicate.
fn find_where<T, F>(items: &[T], predicate: F) -> Option<&T>
where
    F: Fn(&T) -> bool,
{
    items.iter().find(|item| predicate(item))
}

let codes = vec![200, 301, 404, 500];
let first_error = find_where(&codes, |&&c| c >= 400);
// Some(&404)

Generic Enums

You already know the two most important generic enums:

// These are built into the standard library:

enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

// You can define your own:

/// The outcome of routing a request.
enum RouteResult<T> {
    Matched(T),
    NotFound,
    MethodNotAllowed(Vec<Method>),  // which methods ARE allowed
}

Adding Bounds to Implementations

You can implement methods only for specific type parameters. This lets a generic type have extra capabilities when the inner type supports them:

use std::fmt;

impl<V: fmt::Display> Cache<V> {
    /// Only available when V implements Display
    fn dump(&self) {
        for (key, value) in &self.data {
            println!("{}: {}", key, value);
        }
    }
}

// Cache<String> has .dump() because String: Display ✓
// Cache<TcpStream> does NOT have .dump() because TcpStream: Display ✗

Trait Objects (dyn Trait) vs Static Dispatch

There are two ways to use traits with generics, and the distinction matters for performance and flexibility:

Static Dispatch (Generics)

When you use impl Trait or <T: Trait>, the compiler generates a separate copy of the function for each concrete type used. This is called monomorphization:

fn send(response: &impl ToHttp, stream: &mut TcpStream) {
    stream.write_all(&response.to_http()).unwrap();
}

// The compiler generates:
// fn send_Response(response: &Response, ...) { ... }
// fn send_str(response: &&str, ...) { ... }
// Each is specialized and inlined - zero overhead.

Pros: Zero runtime cost, compiler can inline and optimize. Cons: Code bloat if used with many types; can't store different types in one collection.

Dynamic Dispatch (Trait Objects)

When you use dyn Trait, the compiler stores a pointer to the value and a pointer to a vtable (a table of function pointers). The method call goes through the vtable at runtime:

fn send(response: &dyn ToHttp, stream: &mut TcpStream) {
    stream.write_all(&response.to_http()).unwrap();
}

// One copy of the function - method lookup at runtime via vtable.
// Slightly slower, but the function isn't duplicated for each type.

The key advantage: you can store different types in the same collection:

// This is impossible with generics - all elements must be the same type
// let items: Vec<impl ToHttp> = vec![response, "hello"]; // ✗

// But trait objects can hold different types:
let items: Vec<Box<dyn ToHttp>> = vec![
    Box::new(Response::ok("page")),
    Box::new("hello"),
];

for item in &items {
    let bytes = item.to_http();
    println!("Sending {} bytes", bytes.len());
}

Pros: Heterogeneous collections, smaller binary, runtime flexibility. Cons: Small runtime cost (vtable lookup), can't inline, needs Box allocation.

Tip
Default to static dispatch (impl Trait / generics) - it's faster and the compiler checks everything at compile time. Use dyn Trait when you need to store different types together or when you're building a plugin system where the types aren't known at compile time.

When to Use Which

SituationUse
Function takes one concrete type (caller chooses)impl Trait / <T>
Collection of different types sharing a traitVec<Box<dyn Trait>>
Function returns one of several typesBox<dyn Trait>
Maximum performance, known types<T: Trait>
Plugin or handler registrationBox<dyn Trait>

Applying Traits: A Handler Trait for Route Handlers

In Chapter 7, we used type Handler = fn(&Request) -> Result<Response, ServerError> - a function pointer. This works but is limiting: function pointers can't capture state. A handler that needs access to a database connection or config can't be a plain function pointer.

Let's replace it with a trait:

src/handler.rs
use crate::error::ServerError;
use crate::request::Request;
use crate::response::Response;

/// Anything that can handle an HTTP request.
pub trait Handler {
    fn handle(&self, request: &Request) -> Result<Response, ServerError>;
}

// Functions still work - implement Handler for function pointers
impl<F> Handler for F
where
    F: Fn(&Request) -> Result<Response, ServerError>,
{
    fn handle(&self, request: &Request) -> Result<Response, ServerError> {
        (self)(request)
    }
}

That impl<F> Handler for F is a blanket implementation - it implements Handler for any function (or closure) with the right signature. This means all our existing handler functions work without changes.

But now we can also create stateful handlers:

/// A handler that serves a static string.
struct StaticPage {
    content_type: &'static str,
    body: &'static str,
}

impl Handler for StaticPage {
    fn handle(&self, _req: &Request) -> Result<Response, ServerError> {
        let mut resp = Response::new(200, "OK", self.body);
        resp.add_header("Content-Type", self.content_type);
        Ok(resp)
    }
}

/// A handler that counts requests.
struct Counter {
    name: String,
}

impl Handler for Counter {
    fn handle(&self, _req: &Request) -> Result<Response, ServerError> {
        // In a real server, you'd use atomic counters or a mutex
        let body = format!("<h1>Counter: {}</h1>", self.name);
        Ok(Response::html(&body))
    }
}

Now update the router to use trait objects so it can store different handler types:

src/router.rs
use std::collections::HashMap;

use crate::error::ServerError;
use crate::handler::Handler;
use crate::request::{Method, Request};
use crate::response::Response;

pub struct Router {
    routes: HashMap<(Method, String), Box<dyn Handler>>,
}

impl Router {
    pub fn new() -> Self {
        Router { routes: HashMap::new() }
    }

    pub fn add<H: Handler + 'static>(
        &mut self,
        method: Method,
        path: &str,
        handler: H,
    ) {
        self.routes.insert(
            (method, path.to_string()),
            Box::new(handler),
        );
    }

    pub fn route(&self, request: &Request) -> Result<Response, ServerError> {
        let key = (request.method.clone(), request.path.clone());
        match self.routes.get(&key) {
            Some(handler) => handler.handle(request),
            None => Err(ServerError::NotFound(
                format!("{} {}", request.method, request.path)
            )),
        }
    }
}

Key changes from the function-pointer version:

  • Box<dyn Handler> replaces the function pointer. Each route stores a boxed trait object - it could be a function, a closure, or a struct like StaticPage.
  • add<H: Handler + 'static> is generic - it accepts any type that implements Handler. The 'static bound means the handler must own its data (no borrowed references that could dangle).
  • handler.handle(request) dispatches through the vtable at runtime. This is a tiny cost compared to the actual request handling work.

Registration looks the same - but now supports more patterns:

fn build_router() -> Router {
    let mut router = Router::new();

    // Plain functions - still work via blanket impl
    router.add(Method::Get, "/", home);
    router.add(Method::Get, "/about", about);

    // Struct handlers - carry their own state
    router.add(Method::Get, "/readme", StaticPage {
        content_type: "text/plain",
        body: "This is a Rust webserver.",
    });

    // Closures - capture variables from the environment
    let version = "0.1.0";
    router.add(Method::Get, "/version", move |_req: &Request| {
        Ok(Response::html(&format!("<p>v{}</p>", version)))
    });

    router
}

fn home(_req: &Request) -> Result<Response, ServerError> {
    Ok(Response::html("<h1>Welcome!</h1>"))
}

fn about(_req: &Request) -> Result<Response, ServerError> {
    Ok(Response::html("<h1>About</h1>"))
}
Note
The blanket implementation impl<F> Handler for F where F: Fn(...) is a common Rust pattern. The standard library uses it too - Iterator::map accepts any FnMut, not a specific closure type. It bridges traits and closures seamlessly.

Exercise

  1. Implement a Middleware trait with a method fn process(&self, req: &Request, next: &dyn Handler) -> Result<Response, ServerError>. Create a Logger middleware that prints the request, calls next.handle(req), and prints the response status.
  2. Write a generic function fn try_parse<T: FromStr>(s: &str) -> Option<T> that attempts to parse any type from a string. Use it to parse port numbers, status codes, and content lengths.
  3. Create a FileHandler struct with a root_dir: String field. Implement Handler so it serves files relative to root_dir. (We'll do proper file serving in Chapter 15 - for now, just construct the file path from the request path.)
  4. Experiment with the Cache<V> from earlier. Add a bound: impl<V: Clone> Cache<V> and write a get_cloned(&self, key: &str) -> Option<V> that returns an owned copy. Why does get return &V but get_cloned requires Clone?
  5. Try removing the 'static bound from Router::add. What error does the compiler give? Why is 'static necessary for Box<dyn Handler>?

What's Next

Traits and generics are the heart of Rust's type system. You can now define shared behavior, write flexible code that works across types, and choose between static and dynamic dispatch based on your needs. Our router stores Box<dyn Handler>, accepting functions, closures, and custom handler structs through a single trait.

In the next chapter, we'll go deeper into closures and iterators - how closures capture variables, the three Fn traits, and how to build a middleware pipeline using closures that wrap handlers.