GATs: Generic Associated Types em ação

1. Introdução às Generic Associated Types

Generic Associated Types (GATs) representam uma das adições mais significativas à linguagem Rust desde sua estabilização na versão 1.65. Enquanto associated types comuns permitem que traits definam tipos que são determinados pela implementação, GATs estendem esse conceito permitindo que esses tipos sejam parametrizados por lifetimes ou outros tipos genéricos.

A diferença fundamental é sutil mas poderosa: com associated types comuns, você declara type Item; onde Item é fixo para uma implementação. Com GATs, você declara type Item<'a>; onde Item pode variar dependendo do lifetime 'a. Isso resolve problemas clássicos de abstração em Rust, especialmente quando lidamos com referências e lifetimes em traits.

2. Sintaxe e Declaração de GATs

A sintaxe básica para declarar um trait com GAT é direta:

trait Container {
    type Item<'a>;

    fn get(&self) -> Self::Item<'_>;
}

Implementar GATs para tipos concretos segue um padrão semelhante a associated types comuns, mas com a adição dos parâmetros genéricos:

struct VecWrapper<T>(Vec<T>);

impl<T> Container for VecWrapper<T> {
    type Item<'a> = &'a T;

    fn get(&self) -> Self::Item<'_> {
        &self.0[0]
    }
}

Restrições adicionais podem ser aplicadas usando cláusulas where:

trait AdvancedContainer where Self: Sized {
    type Item<'a> where Self: 'a;

    fn borrow(&self) -> Self::Item<'_>;
}

3. GATs com Lifetimes: O Caso Clássico

O caso de uso mais emblemático para GATs é a abstração de referências com lifetimes em traits. Antes das GATs, implementar um LendingIterator (um iterador que empresta dados) era impossível sem soluções complexas.

trait LendingIterator {
    type Item<'a> where Self: 'a;

    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

struct WindowMut<'a, T> {
    data: &'a mut [T],
    index: usize,
    size: usize,
}

impl<'a, T> LendingIterator for WindowMut<'a, T> {
    type Item<'b> = &'b mut [T] where Self: 'b;

    fn next<'b>(&'b mut self) -> Option<Self::Item<'b>> {
        if self.index + self.size > self.data.len() {
            return None;
        }
        let window = &mut self.data[self.index..self.index + self.size];
        self.index += 1;
        Some(window)
    }
}

Este exemplo demonstra como GATs permitem que cada chamada a next() retorne uma referência com um lifetime diferente, algo impossível com associated types tradicionais.

4. GATs com Parâmetros de Tipo

GATs não se limitam a lifetimes — podem aceitar tipos como parâmetros, permitindo construções mais abstratas:

trait Functor {
    type Output<T>;

    fn map<F, A, B>(self, f: F) -> Self::Output<B>
    where
        F: FnOnce(A) -> B,
        Self: Sized;
}

impl<T> Functor for Option<T> {
    type Output<U> = Option<U>;

    fn map<F, A, B>(self, f: F) -> Self::Output<B>
    where
        F: FnOnce(A) -> B,
    {
        self.map(f)
    }
}

Antes das GATs, implementar um trait Monad genérico exigia truques com macros ou trait objects. Agora é possível:

trait Monad: Functor {
    type Wrap<T>;

    fn unit<T>(value: T) -> Self::Wrap<T>;
    fn bind<F, A, B>(self, f: F) -> Self::Wrap<B>
    where
        F: FnOnce(A) -> Self::Wrap<B>,
        Self: Sized;
}

5. GATs e Pattern Matching em Traits

GATs permitem modelar tipos que dependem da implementação de forma mais flexível. O problema clássico do "streaming iterator" — onde diferentes implementações precisam retornar diferentes tipos de iteradores — é resolvido elegantemente:

trait Collection {
    type Item;
    type Iter<'a>: Iterator<Item = &'a Self::Item> where Self: 'a;
    type IterMut<'a>: Iterator<Item = &'a mut Self::Item> where Self: 'a;

    fn iter(&self) -> Self::Iter<'_>;
    fn iter_mut(&mut self) -> Self::IterMut<'_>;
}

struct MyVec<T>(Vec<T>);

impl<T> Collection for MyVec<T> {
    type Item = T;
    type Iter<'a> = std::slice::Iter<'a, T>;
    type IterMut<'a> = std::slice::IterMut<'a, T>;

    fn iter(&self) -> Self::Iter<'_> {
        self.0.iter()
    }

    fn iter_mut(&mut self) -> Self::IterMut<'_> {
        self.0.iter_mut()
    }
}

6. GATs em Ação: Casos de Uso Avançados

GATs são fundamentais para o suporte nativo a async iterators:

trait AsyncIterator {
    type Item<'a> where Self: 'a;

    fn poll_next<'a>(
        self: Pin<&'a mut Self>,
        cx: &mut Context<'_>,
    ) -> Poll<Option<Self::Item<'a>>>;
}

Bibliotecas populares já adotaram GATs extensivamente:
- tower: Usa GATs para definir serviços com diferentes tipos de resposta dependendo do lifetime da requisição
- bevy: Utiliza GATs em seus sistemas de query para abstrair diferentes modos de acesso a componentes
- diesel: Emprega GATs para modelar relações entre tabelas de forma mais segura

Padrões de design como type families se beneficiam diretamente:

trait TypeFamily {
    type WithLifetime<'a>;
}

struct RefFamily;

impl TypeFamily for RefFamily {
    type WithLifetime<'a> = &'a ();
}

7. Limitações e Armadilhas das GATs

Apesar do poder, GATs têm limitações importantes. A inferência de tipos ainda é restrita:

// Pode não compilar em versões antigas
fn example<T: Container>(c: T) {
    let item = c.get(); // Tipo pode não ser inferido corretamente
}

Problemas com lifetimes não nomeados e elisão podem causar confusão:

trait Problematic {
    type Item<'a>;

    // Isso pode não funcionar como esperado
    fn get(&self) -> Self::Item<'_>;
}

Workarounds comuns incluem especificar explicitamente lifetimes e usar cláusulas where:

trait Robust {
    type Item<'a> where Self: 'a;

    fn get<'b>(&'b self) -> Self::Item<'b> where 'b: '_;
}

8. Futuro das GATs e Alternativas

O futuro das GATs está ligado a features como type alias impl Trait (TAIT), que permitirá maior flexibilidade na definição de tipos associados. Const generics e type-level programming continuam evoluindo, oferecendo alternativas para casos específicos.

A escolha entre GATs, macros e trait objects depende do caso:
- GATs: Quando você precisa de abstração sobre lifetimes ou tipos genéricos em traits
- Macros: Para geração de código repetitivo sem necessidade de abstração de tipos
- Trait objects: Para dispatch dinâmico onde tipos concretos são desconhecidos

GATs representam um passo importante na maturidade do sistema de tipos de Rust, permitindo expressar padrões que antes exigiam soluções complexas ou impossíveis.

Referências