Los Generic Associated Types (GATs), estabilizados en Rust 1.65, permiten que los tipos asociados en un trait lleven parámetros genéricos propios, incluyendo lifetimes. Antes de los GATs, ciertos patrones de abstracción simplemente no se podían expresar en Rust estable.
El ejemplo más conocido que motivó los GATs es el streaming iterator o lending iterator: un iterador que presta (lends) sus elementos en lugar de devolverlos por valor. Con el trait Iterator estándar, cada elemento debe ser independiente del iterador. Con GATs, el elemento puede vivir tanto como la referencia al iterador.
El problema sin GATs: LendingIterator
Imagina que quieres un iterador sobre líneas de un fichero que evite copiar cada línea. El iterador reutiliza el mismo buffer para cada línea:
// Lo que queremos hacer:
trait LendingIterator {
type Item<'a> where Self: 'a; // <-- GAT: Item lleva un lifetime propio
fn next(&mut self) -> Option<Self::Item<'_>>;
}
// Sin GATs, esto no es posible: no podemos expresar
// que Item depende del lifetime de &mut self
Con el trait Iterator estándar, next(&mut self) -> Option<Self::Item> exige que Item sea independiente del iterador. No puedes devolver una referencia al interior del iterador. Con GATs, sí puedes.
GATs en acción: un LendingIterator real
use std::io::{BufRead, BufReader, Read};
// El trait con GAT
trait LendingIterator {
type Item<'a> where Self: 'a;
fn next(&mut self) -> Option<Self::Item<'_>>;
}
// Implementación: iterador sobre líneas de un BufReader
struct LineasReader<R: Read> {
reader: BufReader<R>,
buffer: String,
}
impl<R: Read> LineasReader<R> {
pub fn new(reader: R) -> Self {
LineasReader {
reader: BufReader::new(reader),
buffer: String::new(),
}
}
}
impl<R: Read> LendingIterator for LineasReader<R> {
type Item<'a> = &'a str where R: 'a;
fn next(&mut self) -> Option<&str> {
self.buffer.clear();
match self.reader.read_line(&mut self.buffer) {
Ok(0) => None, // EOF
Ok(_) => {
// Devolvemos una referencia al buffer interno
// El caller la usa y luego llamará a next() de nuevo
Some(self.buffer.trim_end_matches('n'))
}
Err(_) => None,
}
}
}
fn contar_lineas(mut iter: impl for<'a> LendingIterator<Item<'a> = &'a str>) -> usize {
let mut count = 0;
while iter.next().is_some() {
count += 1;
}
count
}
GATs para colecciones que prestan sus elementos
Otro caso clásico es un trait para colecciones donde los elementos son referencias al interior de la colección:
trait Coleccion {
type Elemento<'a> where Self: 'a;
fn obtener(&self, idx: usize) -> Option<Self::Elemento<'_>>;
fn longitud(&self) -> usize;
}
// Vec implementa el trait devolviendo referencias
impl<T> Coleccion for Vec<T> {
type Elemento<'a> = &'a T where T: 'a;
fn obtener(&self, idx: usize) -> Option<&T> {
self.get(idx)
}
fn longitud(&self) -> usize {
self.len()
}
}
// Una implementación que devuelve valores calculados
struct RangoColeccion {
desde: i32,
hasta: i32,
}
impl Coleccion for RangoColeccion {
type Elemento<'a> = i32; // No es una referencia, es un valor
fn obtener(&self, idx: usize) -> Option<i32> {
let valor = self.desde + idx as i32;
if valor <= self.hasta { Some(valor) } else { None }
}
fn longitud(&self) -> usize {
(self.hasta - self.desde + 1).max(0) as usize
}
}
GATs con múltiples parámetros
trait Transformable {
type Entrada<'a> where Self: 'a;
type Salida<'a> where Self: 'a;
fn transformar<'a>(&'a self, entrada: Self::Entrada<'a>) -> Self::Salida<'a>;
}
struct MayusculasTransformer;
impl Transformable for MayusculasTransformer {
type Entrada<'a> = &'a str;
type Salida<'a> = String; // No presta, devuelve owned
fn transformar<'a>(&'a self, entrada: &'a str) -> String {
entrada.to_uppercase()
}
}
GATs y const generics combinados
trait ArrayContainer {
type Buffer<const N: usize>;
fn nuevo<const N: usize>() -> Self::Buffer<N>;
}
struct StackAllocator;
impl ArrayContainer for StackAllocator {
type Buffer<const N: usize> = [u8; N];
fn nuevo<const N: usize>() -> [u8; N] {
[0u8; N]
}
}
struct HeapAllocator;
impl ArrayContainer for HeapAllocator {
type Buffer<const N: usize> = Vec<u8>;
fn nuevo<const N: usize>() -> Vec<u8> {
vec![0u8; N]
}
}
Limitaciones actuales de los GATs
Los GATs en Rust 1.65+ tienen algunas limitaciones conocidas:
- El compilador a veces requiere constraints adicionales (
where Self: 'a) que deberían inferirse automáticamente. Los mensajes de error cuando faltan pueden ser confusos. - Implementar GATs en objetos trait (
dyn Trait) no siempre funciona. - Algunos patrones avanzados de GATs con HRTBs (Higher-Rank Trait Bounds) tienen limitaciones.
El equipo de Rust está trabajando en mejorar la ergonomía de los GATs. Para la mayoría de casos prácticos actuales, los GATs funcionan bien y son estables.
Cuándo usar GATs
Los GATs son la herramienta correcta cuando necesitas:
- Un trait que devuelva referencias con lifetimes ligados al receptor (
&mut self). - Abstracciones de colección donde los elementos pueden ser referencias o valores según la implementación.
- Traits para transformaciones que pueden preservar o cambiar los lifetimes de los datos.
- Parsers o deserializadores que evitan copias prestando sus resultados.
Para abstracciones más simples donde los lifetimes no son el problema, los tipos asociados sin GATs o simplemente los genéricos son suficientes y producen errores de compilación más claros.
