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 Erroris 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'connof 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