Iterator Pattern em Zig — O que é e Como Usar

Iterator Pattern em Zig — O que é e Como Usar

Definição

O Iterator Pattern (padrão de iteração) em Zig segue a convenção de expor um método next() que retorna ?T — ou seja, um optional que é null quando a iteração termina. Diferente de linguagens com traits/interfaces formais de iterador (como Rust ou Java), Zig usa duck typing via anytype ou convenção explícita. Qualquer struct com um método next() que retorne optional pode ser usada como iterador.

Este padrão é usado extensivamente na biblioteca padrão para tokenização de strings, iteração sobre diretórios, leitura de linhas e muito mais.

Por que Iterator Pattern Importa

  1. Avaliação preguiçosa: Dados são produzidos sob demanda, sem alocar tudo na memória.
  2. Composição: Iteradores podem ser encadeados (map, filter, take).
  3. Convenção simples: Apenas next() -> ?T — sem interface formal complexa.
  4. Uso com while: Integra naturalmente com while (iter.next()) |item|.

Exemplo Prático

Iterador Simples

const std = @import("std");

const Intervalo = struct {
    atual: usize,
    fim: usize,

    pub fn init(inicio: usize, fim: usize) Intervalo {
        return .{ .atual = inicio, .fim = fim };
    }

    pub fn next(self: *Intervalo) ?usize {
        if (self.atual >= self.fim) return null;
        const valor = self.atual;
        self.atual += 1;
        return valor;
    }
};

pub fn main() void {
    var iter = Intervalo.init(5, 10);
    while (iter.next()) |valor| {
        std.debug.print("{} ", .{valor}); // 5 6 7 8 9
    }
    std.debug.print("\n", .{});
}

Tokenizador de Strings (da std lib)

const std = @import("std");

pub fn main() void {
    const csv = "nome,idade,cidade,país";

    // std.mem.splitScalar retorna um iterador
    var iter = std.mem.splitScalar(u8, csv, ',');

    while (iter.next()) |campo| {
        std.debug.print("Campo: '{s}'\n", .{campo});
    }
    // Campo: 'nome'
    // Campo: 'idade'
    // Campo: 'cidade'
    // Campo: 'país'
}

Iterador com Estado Complexo

const std = @import("std");

fn Fibonacci() type {
    return struct {
        a: u64 = 0,
        b: u64 = 1,
        contagem: usize = 0,
        limite: usize,

        const Self = @This();

        pub fn init(limite: usize) Self {
            return .{ .limite = limite };
        }

        pub fn next(self: *Self) ?u64 {
            if (self.contagem >= self.limite) return null;
            self.contagem += 1;

            const resultado = self.a;
            const novo = self.a +| self.b; // saturating add
            self.a = self.b;
            self.b = novo;
            return resultado;
        }
    };
}

pub fn main() void {
    var fib = Fibonacci().init(10);
    while (fib.next()) |valor| {
        std.debug.print("{} ", .{valor});
    }
    // 0 1 1 2 3 5 8 13 21 34
    std.debug.print("\n", .{});
}

Convenção do Iterador

// Qualquer struct com este método é um iterador:
pub fn next(self: *Self) ?ItemType {
    // Retornar próximo item ou null se acabou
}

// Uso idiomático:
while (iter.next()) |item| {
    // processar item
}

Iteradores na std lib

IteradorUso
std.mem.splitScalarDividir string por caractere
std.mem.splitSequenceDividir por sequência
std.mem.tokenizeScalarTokenizar ignorando delimitadores consecutivos
std.mem.windowJanela deslizante sobre slice
std.fs.Dir.iterate()Iterar entradas de diretório

Armadilhas Comuns

  • Mutabilidade: O iterador precisa ser var, não const, pois next() modifica estado interno.
  • Consumo único: A maioria dos iteradores só pode ser percorrida uma vez. Para percorrer novamente, crie outro iterador.
  • Sem reset: Não há convenção de “reset” — crie uma nova instância.
  • null vs fim: null significa “sem mais itens”, não erro. Para iteradores que podem falhar, o retorno deve ser !?T.

Quando Usar Iterator Pattern

Use o padrão de iterador quando:

  • Processamento sequencial: Você precisa percorrer elementos um a um sem carregar tudo na memória.
  • Fonte de dados lazy: Os dados são gerados sob demanda (sequências infinitas, leitura de arquivos, streams de rede).
  • Composição de transformações: Aplicar filtros e mapeamentos em cadeia antes de consumir os resultados.
  • Abstração de origem: A função que consome o iterador não precisa saber se os dados vêm de um array, arquivo ou rede.

Funções Genéricas com Iteradores

Como Zig não tem interfaces formais, use anytype para escrever funções que aceitam qualquer iterador:

const std = @import("std");

// Aceita qualquer tipo que tenha next() -> ?T
fn contar(iter: anytype) usize {
    var count: usize = 0;
    var it = iter;
    while (it.next()) |_| count += 1;
    return count;
}

fn primeiro(iter: anytype) ?@TypeOf(iter).Next {
    var it = iter;
    return it.next();
}

// Coletar todos os itens em um ArrayList
fn coletarEmLista(
    iter: anytype,
    allocator: std.mem.Allocator,
) !std.ArrayList(@TypeOf(blk: {
    var dummy = iter;
    break :blk dummy.next().?;
})) {
    _ = allocator; // simplificado para exemplo
    @compileError("exemplo simplificado");
}

A convenção next() -> ?T funciona como um contrato implícito que o compilador verifica via duck typing.

Boas Práticas

  • Declare como var: O iterador precisa ser mutável, pois next() altera estado interno. const iter = ... causará erro de compilação ao chamar next().
  • Crie novo iterador para reiterar: Não há convenção de reset. Se precisar percorrer novamente, construa uma nova instância.
  • Use ?T limpo para iteradores infalíveis: Se a iteração não pode falhar, retorne ?T. Reserve !?T para iteradores que lêem de fontes que podem dar erro (arquivos, rede).
  • Documente o comportamento de fim: Deixe claro se null significa “sem mais dados” ou se há situações onde o iterador para prematuramente.

Termos Relacionados

  • Optional — Tipo de retorno ?T do next()
  • Struct — Iteradores são structs com estado
  • anytype — Funções genéricas aceitam qualquer iterador
  • Slice — Fonte comum de dados para iteração

Tutoriais Relacionados

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.