GATs: tipos asociados que a su vez son genéricos
Un tipo asociado puede llevar sus propios parámetros —lifetimes o tipos—: es un Generic Associated Type. La sintaxis type Item<'a> desbloquea lo que el Iterator clásico no puede expresar: un iterador que presta datos de sí mismo en cada paso, el LendingIterator. Por qué el Item de Iterator jamás puede tomar prestado del iterador, y cómo un tipo asociado genérico rompe ese muro.
Un tipo asociado nombra un tipo. ¿Y si ese tipo, a su vez, dependiera de un parámetro? Un type Item<'a> no es un tipo, sino una familia de tipos indexada por un lifetime: para cada 'a posible, un Item<'a> distinto. Eso es un Generic Associated Type, o GAT, y su llegada a Rust estable en 2022 —tras seis años de diseño— destrabó un patrón que el sistema de tipos había tenido, hasta entonces, literalmente prohibido: un iterador cuyos elementos toman prestado del propio iterador. Para entender por qué eso era imposible, primero hay que ver con exactitud dónde se rompe el Iterator clásico.
- Declarar un tipo asociado genérico con
type Item<'a>;y comprender que es una familia de tipos. - Diagnosticar por qué el
ItemdeIteratorno puede tomar prestado del iterador. - Definir un
LendingIteratorcuyonextdevuelvaOption<Self::Item<'_>>. - Reconocer la cláusula
where Self: 'acomo parte casi obligada del patrón.
El muro del Iterator clásico
Recuerda la firma exacta del método fundamental de Iterator:
fn next(&mut self) -> Option<Self::Item>;
Self::Item es un tipo fijo, decidido al implementar el trait, sin ninguna conexión con el préstamo &mut self de esta llamada concreta. La consecuencia es severa: el elemento devuelto no puede referirse a nada dentro del iterador, porque su tipo no menciona ningún lifetime ligado a self. Un Item = &T tendría que escribirse &'? T con algún '?, y ese lifetime no existe en la definición de Item. Por eso todo iterador que produce referencias —slice::iter, por ejemplo— presta de la colección subyacente, que vive más que el iterador, nunca del iterador mismo.
Intenta escribir un iterador que reparta ventanas mutables solapadas, o que reutilice un búfer interno entregando &self.buffer en cada paso, y chocas contra el muro:
struct Releedor { buffer: String }
// Queremos: cada next presta el buffer interno... pero Item es un tipo fijo,
// no puede ser &'a str para el 'a de esta llamada. No hay forma de escribirlo.
// impl Iterator for Releedor { type Item = &str; // que 'a? imposible
El problema no es de sintaxis: es que type Item; es un tipo cerrado, y lo que necesitamos es un tipo que se abra al lifetime de cada next.
Abrir el tipo con un parámetro
Un GAT hace exactamente eso: dota al tipo asociado de sus propios parámetros. La familia type Item<'a> produce un tipo distinto para cada lifetime 'a, y ahora next puede ligar el elemento al préstamo de esta llamada:
trait LendingIterator {
type Item<'a> where Self: 'a; // una familia indexada por 'a
fn next(&mut self) -> Option<Self::Item<'_>>; // el '_ es el 'a de esta llamada
}
Lee la firma despacio. El &mut self introduce, de forma anónima, un lifetime 'a: la duración de este préstamo. Self::Item<'_> instancia la familia justo en ese 'a. Así, el elemento devuelto puede contener referencias al interior de self que viven exactamente hasta que vuelvas a llamar a next —momento en que el préstamo &mut self anterior ha de haber terminado—. El compilador impone esa disciplina sola: no puedes conservar el elemento de la iteración anterior mientras pides el siguiente, porque ambos querrían prestar self mutablemente a la vez. El trait presta cada elemento en lugar de entregarlo, y de ahí el nombre: iterador que presta, lending iterator.
struct Ventanas<'v, T> { datos: &'v [T], i: usize, ancho: usize }
impl<'v, T> LendingIterator for Ventanas<'v, T> {
type Item<'a> = &'a [T] where Self: 'a;
fn next(&mut self) -> Option<Self::Item<'_>> {
if self.i + self.ancho <= self.datos.len() {
let v = &self.datos[self.i..self.i + self.ancho];
self.i += 1;
Some(v) // presta una ventana; vive hasta el proximo next
} else {
None
}
}
}
Aunque el caso estrella es type Item<'a>, un tipo asociado puede parametrizarse también por tipos: type Contenedor<T>;. Esto habilita el llamado patrón familia, con el que se emula parcialmente la genericidad de orden superior —hablar de Vec o de HashMap como constructores, no como tipos ya aplicados—. Los lifetimes son el uso dominante en la práctica, pero la maquinaria es la misma: un asociado que recibe argumentos.
Por qué la cláusula where Self: 'a
Casi todo GAT sobre lifetimes arrastra una cláusula where Self: 'a, y conviene entender que no es ritual. Si Item<'a> va a contener referencias al interior de self, entonces self tiene que sobrevivir a 'a: no puedes prestar durante 'a algo que se destruye antes. La cláusula Self: 'a codifica precisamente esa obligación —“Self vive al menos tanto como 'a”— y sin ella el compilador rechaza el impl, porque no podría garantizar que las referencias prestadas sigan siendo válidas.
trait Prestador {
type Ref<'a> where Self: 'a; // Self debe durar tanto como el prestamo
fn primera(&self) -> Self::Ref<'_>;
}
Es la misma lógica de los lifetimes en structs, elevada al tipo asociado: quien presta ha de vivir más que el préstamo. Cuando veas where Self: 'a colgando de un GAT, léela como “esta familia solo tiene sentido para lifetimes que Self alcanza a cubrir”.
flowchart TB subgraph Iterator clasico i1[type Item tipo fijo] --> i2[next devuelve Item sin ligar a self] i2 --> i3[El elemento no puede prestar de self] end subgraph LendingIterator con GAT g1[type Item de a familia por lifetime] --> g2[next devuelve Item del prestamo actual] g2 --> g3[El elemento presta de self hasta el proximo next] end style i3 fill:#f38ba8,color:#11111b style g3 fill:#a6e3a1,color:#11111b style g1 fill:#cba6f7,color:#11111b
Lo que el GAT hace posible
Con el tipo asociado abierto, expresas contratos que antes exigían trucos o eran de plano inexpresables. El elemento de cada paso puede prestar del iterador, así que puedes repartir vistas mutables sucesivas de un búfer, entregar &mut a regiones que cambian entre pasos, o devolver referencias a un estado interno reutilizado sin clonar. Un método por defecto ilustra el cambio de forma: iterar consumiendo cada préstamo antes de pedir el siguiente.
trait LendingIterator {
type Item<'a> where Self: 'a;
fn next(&mut self) -> Option<Self::Item<'_>>;
// Metodo por defecto: consume cada elemento antes del siguiente prestamo
fn para_cada<F>(mut self, mut f: F)
where
Self: Sized,
F: FnMut(Self::Item<'_>),
{
while let Some(x) = self.next() {
f(x); // x vive solo dentro de esta vuelta; luego se puede represtar self
}
}
}
Fíjate en la restricción implícita que el diseño impone: no existe un collect que junte todos los préstamos en un Vec, porque no pueden coexistir —cada uno invalida al anterior—. El GAT no solo añade poder expresivo; codifica en el tipo la imposibilidad de conservar dos préstamos simultáneos, que es justo la garantía que buscabas.
El bucle for x in it exige IntoIterator, cuyo Item es un tipo cerrado sin lifetime; por eso no puedes recorrer un LendingIterator con for. Lo consumes con while let Some(x) = it.next(), que liga y suelta cada préstamo dentro de su propia vuelta. Esa incompatibilidad no es un defecto: es el sistema de tipos recordándote que estos elementos no sobreviven a la iteración, y que por eso no caben en el contrato clásico de Iterator.
Para calibrar la magnitud de los GATs, vuelve a la idea del capítulo primero: un tipo asociado es una función del tipo implementador a un tipo de salida. Un GAT generaliza esa función para que acepte más argumentos —un lifetime, un tipo— y devuelva un miembro distinto de una familia por cada uno. En la jerga de los sistemas de tipos, esto es dar el salto de las funciones de primer orden a las de orden superior sobre tipos: ya no mapeas un tipo a un tipo, sino un tipo a un constructor de tipos. Y ese salto, que en teoría de tipos es enorme, resuelve en Rust un problema concreto y viejísimo. El Iterator clásico tiene una limitación que no es un descuido, sino una imposibilidad estructural: como Item es un tipo cerrado, sin lifetime propio, jamás puede referirse al préstamo de la llamada que lo produjo, y por eso ningún iterador de la biblioteca estándar presta de sí mismo —todos prestan de una colección que les sobrevive—. Durante años esto obligó a acrobacias: clonar cuando querías prestar, o mantener crates enteros —streaming-iterator, lending-iterator— construidos sobre trucos con traits auxiliares y lifetimes en el propio trait que contaminaban cada firma. El GAT disuelve todo eso con una idea mínima: si el tipo asociado pudiera recibir el lifetime de cada next, el elemento podría prestar de self sin contradicción. type Item<'a> es esa idea hecha sintaxis. Que tardara seis años en estabilizarse no fue burocracia, sino la dificultad real de encajar la genericidad de orden superior en un sistema de tipos que debía seguir infiriendo y siendo decidible. Y el dividendo va mucho más allá de los iteradores que prestan: como verás, la propia sintaxis de async fn en traits se apoya en esta maquinaria por debajo. Un GAT no es una esquina exótica del lenguaje; es la pieza que le faltaba al tipo asociado para poder hablar de datos cuya validez está atada al instante en que se producen.
Un GAT es un tipo asociado con sus propios parámetros: type Item<'a> no es un tipo, sino una familia indexada por el lifetime 'a. Desbloquea lo que el Iterator clásico no puede expresar, porque su Item es un tipo cerrado sin conexión con el préstamo &mut self: un LendingIterator cuyo next devuelve Option<Self::Item<'_>> presta cada elemento del propio iterador, válido solo hasta el siguiente next. La cláusula where Self: 'a acompaña casi siempre al patrón y significa que Self sobrevive al préstamo. Estabilizado en Rust 1.65 tras seis años de diseño, el GAT lleva el tipo asociado de función sobre tipos a constructor de tipos, y es la base sobre la que se apoyan patrones que verás después, incluido async en traits.
- Explica, con la firma
fn next(&mut self) -> Option<Self::Item>, por qué elItemde unIteratorclásico no puede contener una referencia a un campo deself. - Define el trait
LendingIteratorcontype Item<'a> where Self: 'ay unnextque devuelvaOption<Self::Item<'_>>; comenta qué papel juega el'_. - Implementa un
LendingIteratorsobre un&[T]que entregue ventanas solapadas de ancho fijo como&'a [T]; comprueba que compila con la cláusulawhere Self: 'a. - Intenta guardar en un
Veclos dos primeros elementos prestados de tuLendingIteratory lee el error; explica por qué el diseño impide conservar dos préstamos a la vez. - Quita la cláusula
where Self: 'ade tu trait y describe qué garantiza el compilador que ya no puede probar, conectándolo con “quien presta vive más que el préstamo”.