Nesta aula, vamos explorar em profundidade alguns dos traits mais importantes da biblioteca padrão do Rust: Iterator, IntoIterator, FromIterator e Extend. Esses traits formam a espinha dorsal do processamento de coleções e sequências em Rust, permitindo que você escreva código genérico, eficiente e expressivo. Compreender esses traits é essencial para dominar a linguagem e aproveitar ao máximo o ecossistema de crates.

Vamos começar com uma visão geral de cada trait, suas relações e como eles se integram no dia a dia. Em seguida, mergulharemos em exemplos práticos, mostrando como implementá-los em seus próprios tipos e como usá-los em combinação com os métodos da biblioteca padrão. Ao final, você terá uma compreensão sólida de como criar e manipular sequências de dados de forma idiomática em Rust.

Iterator estendido

O trait Iterator é fundamental em Rust. Ele define um protocolo para produzir uma sequência de valores. A implementação mínima exige apenas o método next(), que retorna Option<Self::Item>. No entanto, a biblioteca padrão fornece dezenas de métodos (como map, filter, fold, etc.) que são implementados automaticamente para qualquer tipo que implemente Iterator. Esses métodos são chamados de adaptadores e consomem ou transformam o iterador.

Vamos criar um iterador personalizado que gera uma sequência de números primos. Isso nos permitirá ver como implementar next() e como podemos usar os métodos fornecidos pelo trait.

struct PrimeIterator {
    current: u64,
}

impl PrimeIterator {
    fn new() -> Self {
        PrimeIterator { current: 2 }
    }

    fn is_prime(n: u64) -> bool {
        if n < 2 {
            return false;
        }
        for i in 2..=(n as f64).sqrt() as u64 {
            if n % i == 0 {
                return false;
            }
        }
        true
    }
}

impl Iterator for PrimeIterator {
    type Item = u64;

    fn next(&mut self) -> Option<Self::Item> {
        while !Self::is_prime(self.current) {
            self.current += 1;
        }
        let prime = self.current;
        self.current += 1;
        Some(prime)
    }
}

fn main() {
    let primes = PrimeIterator::new();
    for p in primes.take(10) {
        println!("{}", p);
    }
}

No exemplo acima, implementamos um iterador que gera primos infinitos. Usamos take(10) para consumir apenas os dez primeiros. Isso demonstra como os métodos do trait Iterator (como take) são fornecidos gratuitamente para qualquer iterador.

Além disso, podemos usar métodos como filter, map, enumerate, zip, collect e muitos outros para compor operações complexas de forma legível e eficiente. Por exemplo, podemos usar filter para obter apenas primos maiores que 100:

let big_primes = PrimeIterator::new()
    .filter(|&p| p > 100)
    .take(5);

É importante notar que muitos métodos consomem o iterador (como collect), enquanto outros são preguiçosos (como map e filter) e só executam quando o iterador é consumido. Essa distinção é crucial para escrever código eficiente.

IntoIterator

O trait IntoIterator é a ponte entre tipos que podem ser convertidos em um iterador e o próprio iterador. Ele é implementado por coleções como Vec, HashMap, String, etc. Quando você usa um for loop sobre um valor, o Rust automaticamente chama into_iter().

A assinatura do trait é:

pub trait IntoIterator {
    type Item;
    type IntoIter: Iterator<Item = Self::Item>;
    fn into_iter(self) -> Self::IntoIter;
}

Existem três formas comuns de obter um iterador de uma coleção: iter() (empresta), iter_mut() (empresta mutavelmente) e into_iter() (consome). O trait IntoIterator é implementado para referências a coleções também, permitindo que for loops funcionem de maneiras diferentes.

Vamos ver um exemplo que implementa IntoIterator para um tipo personalizado. Suponha que temos uma estrutura Line que representa uma linha de texto e queremos iterar sobre suas palavras.

struct Line {
    text: String,
}

impl IntoIterator for Line {
    type Item = String;
    type IntoIter = std::vec::IntoIter<String>;

    fn into_iter(self) -> Self::IntoIter {
        self.text
            .split_whitespace()
            .map(|s| s.to_string())
            .collect::<Vec<_>>()
            .into_iter()
    }
}

fn main() {
    let line = Line { text: String::from("Hello world from Rust") };
    for word in line {
        println!("{}", word);
    }
}

No código acima, implementamos IntoIterator para Line, permitindo que um for loop itere sobre as palavras. Observe que usamos collect para transformar o iterador de &str em um Vec<String> e depois into_iter() para obter um iterador que possui os valores.

Também é comum implementar IntoIterator para referências (&Line e &mut Line) para permitir iterar sem consumir a estrutura. Isso é feito implementando o trait para os tipos de referência.

impl<'a> IntoIterator for &'a Line {
    type Item = &'a str;
    type IntoIter = std::str::SplitWhitespace<'a>;

    fn into_iter(self) -> Self::IntoIter {
        self.text.split_whitespace()
    }
}

impl<'a> IntoIterator for &'a mut Line {
    type Item = &'a mut str;
    type IntoIter = std::str::SplitWhitespaceMut<'a>; // não existe na std, mas ilustrativo

    fn into_iter(self) -> Self::IntoIter {
        self.text.split_whitespace_mut()
    }
}

Na prática, você pode usar for word in &line para obter referências às palavras sem consumir a linha.

FromIterator

O trait FromIterator permite construir uma coleção a partir de um iterador. Ele é implementado por todos os tipos de coleção padrão, como Vec, HashMap, String, etc. O método principal é from_iter, que recebe um iterador e retorna a coleção.

A assinatura é:

pub trait FromIterator<A>: Sized {
    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self;
}

O método collect é o principal consumidor de FromIterator. Quando você chama iter.collect(), o tipo de destino é inferido pelo contexto, e o Rust chama FromIterator::from_iter para construir a coleção.

Vamos criar um tipo personalizado que implementa FromIterator para coletar números em uma estrutura que armazena soma e quantidade.

struct Stats {
    sum: f64,
    count: u64,
}

impl Stats {
    fn new() -> Self {
        Stats { sum: 0.0, count: 0 }
    }

    fn add(&mut self, value: f64) {
        self.sum += value;
        self.count += 1;
    }

    fn average(&self) -> Option<f64> {
        if self.count == 0 {
            None
        } else {
            Some(self.sum / self.count as f64)
        }
    }
}

impl FromIterator<f64> for Stats {
    fn from_iter<T: IntoIterator<Item = f64>>(iter: T) -> Self {
        let mut stats = Stats::new();
        for value in iter {
            stats.add(value);
        }
        stats
    }
}

fn main() {
    let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
    let stats: Stats = data.iter().cloned().collect();
    println!("Média: {:?}", stats.average());
}

No exemplo, implementamos FromIterator para Stats, permitindo usar collect() para construir um objeto Stats a partir de um iterador de f64.

É importante notar que FromIterator é usado internamente por muitos métodos, como collect, e também pode ser usado diretamente. Isso é útil quando você quer construir uma coleção com uma lógica personalizada.

Extend

O trait Extend permite adicionar elementos de um iterador a uma coleção existente. Ele é implementado por coleções mutáveis, como Vec, HashMap, String, etc. O método principal é extend, que recebe um iterador e adiciona os elementos à coleção.

A assinatura é:

pub trait Extend<A> {
    fn extend<T: IntoIterator<Item = A>>(&mut self, iter: T);
}

O método extend é frequentemente usado com Vec, por exemplo, para adicionar vários elementos de uma vez. Também é a base para o operador += em alguns contextos, mas principalmente é usado explicitamente.

Vamos implementar Extend para nosso tipo Stats:

impl Extend<f64> for Stats {
    fn extend<T: IntoIterator<Item = f64>>(&mut self, iter: T) {
        for value in iter {
            self.add(value);
        }
    }
}

Agora podemos usar extend para adicionar mais dados a um Stats existente:

fn main() {
    let mut stats = Stats::new();
    stats.extend(vec![1.0, 2.0, 3.0]);
    stats.extend(vec![4.0, 5.0]);
    println!("Média: {:?}", stats.average());
}

O trait Extend é particularmente útil quando você está construindo coleções incrementalmente ou combinando múltiplas fontes de dados. Ele também é usado em combinação com collect para construir coleções de forma eficiente.

Boas práticas e observações finais

Ao implementar esses traits, lembre-se de que eles são parte do contrato de uma coleção. Sempre que você criar um tipo que representa uma coleção, considere implementar IntoIterator, FromIterator e Extend para que ele se comporte de forma consistente com as coleções padrão.

Além disso, aproveite os métodos fornecidos pelo trait Iterator para compor operações de forma declarativa, em vez de escrever loops manuais. Isso torna o código mais legível e menos propenso a erros.

Por fim, lembre-se de que collect pode ser usado para converter entre diferentes tipos de coleção, graças a FromIterator. Por exemplo, você pode coletar um iterador de pares em um HashMap.

Referências

Exercícios

  1. Exercício 1: Implemente um iterador personalizado chamado Fibonacci que gere os números de Fibonacci infinitamente. Use o método take para imprimir os 10 primeiros números.
  2. ✓ Resposta:
    struct Fibonacci {
        a: u64,
        b: u64,
    }
    
    impl Fibonacci {
        fn new() -> Self {
            Fibonacci { a: 0, b: 1 }
        }
    }
    
    impl Iterator for Fibonacci {
        type Item = u64;
    
        fn next(&mut self) -> Option<Self::Item> {
            let current = self.a;
            self.a = self.b;
            self.b = current + self.b;
            Some(current)
        }
    }
    
    fn main() {
        for fib in Fibonacci::new().take(10) {
            println!("{}", fib);
        }
    }
    
  3. Exercício 2: Crie uma struct RangeStep que itere sobre um intervalo de números com um passo específico. Implemente Iterator para ela. Teste com um intervalo de 0 a 10 com passo 2.
  4. ✓ Resposta:
    struct RangeStep {
        current: i32,
        end: i32,
        step: i32,
    }
    
    impl RangeStep {
        fn new(start: i32, end: i32, step: i32) -> Self {
            RangeStep { current: start, end, step }
        }
    }
    
    impl Iterator for RangeStep {
        type Item = i32;
    
        fn next(&mut self) -> Option<Self::Item> {
            if self.current >= self.end {
                None
            } else {
                let value = self.current;
                self.current += self.step;
                Some(value)
            }
        }
    }
    
    fn main() {
        for x in RangeStep::new(0, 10, 2) {
            println!("{}", x);
        }
    }
    
  5. Exercício 3: Implemente IntoIterator para uma struct Book que contém um Vec<String> de páginas. O iterador deve produzir referências às páginas (&String).
  6. ✓ Resposta:
    struct Book {
        pages: Vec<String>,
    }
    
    impl Book {
        fn new(pages: Vec<String>) -> Self {
            Book { pages }
        }
    }
    
    impl<'a> IntoIterator for &'a Book {
        type Item = &'a String;
        type IntoIter = std::slice::Iter<'a, String>;
    
        fn into_iter(self) -> Self::IntoIter {
            self.pages.iter()
        }
    }
    
    fn main() {
        let book = Book::new(vec![String::from("Page 1"), String::from("Page 2")]);
        for page in &book {
            println!("{}", page);
        }
    }
    
  7. Exercício 4: Crie uma struct Point com coordenadas x e y. Implemente FromIterator<f64> para que possamos coletar um iterador de duas f64 em um Point. Use collect para criar um Point a partir de um vetor de dois números.
  8. ✓ Resposta:
    struct Point {
        x: f64,
        y: f64,
    }
    
    impl FromIterator<f64> for Point {
        fn from_iter<T: IntoIterator<Item = f64>>(iter: T) -> Self {
            let mut values = iter.into_iter();
            let x = values.next().expect("Expected at least one value");
            let y = values.next().expect("Expected at least two values");
            Point { x, y }
        }
    }
    
    fn main() {
        let coords = vec![3.0, 4.0];
        let point: Point = coords.into_iter().collect();
        println!("x: {}, y: {}", point.x, point.y);
    }
    
  9. Exercício 5: Implemente Extend<String> para uma struct TextBuffer que armazena uma String concatenada. Use extend para adicionar várias strings ao buffer.
  10. ✓ Resposta:
    struct TextBuffer {
        content: String,
    }
    
    impl TextBuffer {
        fn new() -> Self {
            TextBuffer { content: String::new() }
        }
    
        fn push_str(&mut self, s: &str) {
            self.content.push_str(s);
        }
    }
    
    impl Extend<String> for TextBuffer {
        fn extend<T: IntoIterator<Item = String>>(&mut self, iter: T) {
            for s in iter {
                self.push_str(&s);
            }
        }
    }
    
    fn main() {
        let mut buffer = TextBuffer::new();
        buffer.extend(vec![String::from("Hello "), String::from("world!")]);
        println!("{}", buffer.content);
    }