Advanced Traits

Associated Types and GATs

In trait design, associated types (type Output;) and Generic Associated Types (GATs, type Output<'a>; or type Output<T>;) express functional dependencies from a trait's implementing type (Self) to its companion output types.

Choosing Between Generic Trait Parameters, Associated Types, and GATs

How many implementations of `Trait` should exist for a single `Self` type?
  |
  +-- MANY (e.g. `From<u8>`, `From<u16>` on `u64`):
  |     ---> Use a Generic Trait Parameter: `trait From<T>`
  |
  +-- ONE per `Self` type:
        +-- Does the output type depend on a call-site borrow lifetime `'a` or element `U`?
              +-- NO  ---> Plain Associated Type: `type Item;`
              +-- YES ---> Generic Associated Type (GAT): `type Item<'a> where Self: 'a;`
Trait Design Mechanism Cardinality per Self Caller Must Specify Type Parameter in Bounds? Dyn-Compatible (dyn Trait)?
Generic Parameter (trait Graph<N, E>) Many (N, E) pairs per Self Yes (G: Graph<Node, Edge>) Yes (once N, E are specified: dyn Graph<N, E>)
Associated Type (trait Graph { type N; type E; }) Exactly 1 (N, E) per Self No (G: Graph suffices; G::N is inferred) Yes, if specified (dyn Graph<N = u32, E = u32>)
GAT (trait Lending { type Item<'a>; }) 1 family parameterized by 'a / T per Self No No (unless GAT parameters are non-generic)

Combining Associated Types and GATs in Streaming / Database Traits

A database connection trait illustrates how plain associated types and GATs work together inside a single trait:

  • type Error is a plain associated type because the error type is fixed for a given backend (PostgresError).
  • type Row<'conn> is a lifetime GAT because a zero-copy database row borrows directly from the connection's network read buffer for the duration 'conn of the query:
use std::fmt::Debug;

trait DatabaseDriver {
    type Error: Debug;
    type Row<'conn>: Debug
    where
        Self: 'conn;

    fn fetch_next<'conn>(&'conn mut self) -> Result<Option<Self::Row<'conn>>, Self::Error>;
}

struct InMemoryDb {
    records: Vec<String>,
    cursor: usize,
}

#[derive(Debug)]
struct ZeroCopyRow<'a> {
    raw_line: &'a str,
}

impl DatabaseDriver for InMemoryDb {
    type Error = &'static str;
    type Row<'conn>
        = ZeroCopyRow<'conn>
    where
        Self: 'conn;

    fn fetch_next<'conn>(&'conn mut self) -> Result<Option<Self::Row<'conn>>, Self::Error> {
        let Some(line) = self.records.get(self.cursor) else {
            return Ok(None);
        };
        self.cursor += 1;
        Ok(Some(ZeroCopyRow {
            raw_line: line.as_str(),
        }))
    }
}

fn scan_rows<D: DatabaseDriver>(db: &mut D) -> Result<usize, D::Error> {
    let mut count = 0;
    while let Some(row) = db.fetch_next()? {
        println!("row #{count}: {:?}", row);
        count += 1;
    }
    Ok(count)
}

fn main() {
    let mut db = InMemoryDb {
        records: vec![String::from("1,alice"), String::from("2,bob")],
        cursor: 0,
    };
    let total = scan_rows(&mut db).unwrap();
    println!("scanned {} rows", total);
}
row #0: ZeroCopyRow { raw_line: "1,alice" }
row #1: ZeroCopyRow { raw_line: "2,bob" }
scanned 2 rows