Cheatsheet: Strategy em Zig

Strategy em Zig

O padrão Strategy define uma família de algoritmos, encapsula cada um deles e os torna intercambiáveis. Em Zig, existem três formas principais de implementar: comptime (zero custo em runtime), ponteiros de função (flexível em runtime) e tagged unions (type-safe com exaustividade verificada pelo compilador).

Quando Usar

  • Diferentes algoritmos de ordenação, compressão, criptografia
  • Estratégias de retry, cache ou roteamento
  • Formatação de saída (JSON, CSV, XML)
  • Validação com regras configuráveis

Strategy com comptime (Custo Zero)

const std = @import("std");

fn Compressor(comptime estrategia: enum { gzip, lz4, nenhuma }) type {
    return struct {
        pub fn comprimir(dados: []const u8) []const u8 {
            return switch (estrategia) {
                .gzip => {
                    // lógica de compressão gzip
                    _ = dados;
                    return "dados_gzip";
                },
                .lz4 => {
                    _ = dados;
                    return "dados_lz4";
                },
                .nenhuma => dados,
            };
        }
    };
}

// Tipo resolvido em compilação — zero overhead
const CompressorGzip = Compressor(.gzip);
const CompressorLz4 = Compressor(.lz4);

Strategy com Ponteiros de Função

const std = @import("std");

const EstrategiaOrdenacao = *const fn ([]i32) void;

fn bubbleSort(dados: []i32) void {
    for (0..dados.len) |_| {
        for (0..dados.len - 1) |j| {
            if (dados[j] > dados[j + 1]) {
                const temp = dados[j];
                dados[j] = dados[j + 1];
                dados[j + 1] = temp;
            }
        }
    }
}

fn insertionSort(dados: []i32) void {
    for (1..dados.len) |i| {
        const chave = dados[i];
        var j: usize = i;
        while (j > 0 and dados[j - 1] > chave) {
            dados[j] = dados[j - 1];
            j -= 1;
        }
        dados[j] = chave;
    }
}

const Ordenador = struct {
    estrategia: EstrategiaOrdenacao,

    pub fn ordenar(self: *const Ordenador, dados: []i32) void {
        self.estrategia(dados);
    }

    pub fn setEstrategia(self: *Ordenador, nova: EstrategiaOrdenacao) void {
        self.estrategia = nova;
    }
};

pub fn main() void {
    var dados = [_]i32{ 5, 2, 8, 1, 9, 3 };
    var ordenador = Ordenador{ .estrategia = bubbleSort };

    ordenador.ordenar(&dados);
    // Trocar estratégia em runtime
    ordenador.setEstrategia(insertionSort);
}

Strategy com Tagged Union

const std = @import("std");

const FormatoSaida = union(enum) {
    json,
    csv: struct { separador: u8 = ',' },
    texto: struct { largura: u16 = 80 },

    pub fn formatar(self: FormatoSaida, dados: anytype, writer: anytype) !void {
        switch (self) {
            .json => {
                try std.json.stringify(dados, .{}, writer);
                try writer.writeAll("\n");
            },
            .csv => |opts| {
                _ = opts;
                // formatação CSV...
                try writer.writeAll("dados,csv\n");
            },
            .texto => |opts| {
                _ = opts;
                try writer.writeAll("Saída texto\n");
            },
        }
    }
};

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    const formato = FormatoSaida.json;
    try formato.formatar(.{ .nome = "Zig" }, stdout);
}

Strategy com Contexto e Estado

Quando a estratégia precisa de estado próprio (ex: uma estratégia de retry com contador interno), combine com o padrão de interface via anyopaque:

const std = @import("std");

const EstrategiaCache = struct {
    ptr: *anyopaque,
    buscarFn: *const fn (*anyopaque, []const u8) ?[]const u8,
    armazenarFn: *const fn (*anyopaque, []const u8, []const u8) void,

    pub fn buscar(self: EstrategiaCache, chave: []const u8) ?[]const u8 {
        return self.buscarFn(self.ptr, chave);
    }

    pub fn armazenar(self: EstrategiaCache, chave: []const u8, valor: []const u8) void {
        self.armazenarFn(self.ptr, chave, valor);
    }
};

// Estratégia LRU com estado interno
const CacheLRU = struct {
    dados: std.StringHashMap([]const u8),
    capacidade: usize,

    pub fn init(allocator: std.mem.Allocator, cap: usize) CacheLRU {
        return .{ .dados = std.StringHashMap([]const u8).init(allocator), .capacidade = cap };
    }

    pub fn estrategia(self: *CacheLRU) EstrategiaCache {
        return .{ .ptr = self, .buscarFn = buscar, .armazenarFn = armazenar };
    }

    fn buscar(ptr: *anyopaque, chave: []const u8) ?[]const u8 {
        const self: *CacheLRU = @alignCast(@ptrCast(ptr));
        return self.dados.get(chave);
    }

    fn armazenar(ptr: *anyopaque, chave: []const u8, valor: []const u8) void {
        const self: *CacheLRU = @alignCast(@ptrCast(ptr));
        self.dados.put(chave, valor) catch {};
    }
};

Considerações de Performance

  • Comptime strategy tem custo absoluto zero: Compressor(.gzip) resulta em código totalmente especializado — não há despacho dinâmico, não há ponteiro de função. O compilador pode inlinar, vetorizar e otimizar como se o código tivesse sido escrito diretamente.
  • Ponteiro de função vs comptime: a chamada via ponteiro de função (self.estrategia(dados)) impede inlining pelo compilador e evita que o preditor de branch funcione otimamente. Para estratégias chamadas bilhões de vezes (ordenação de dados, compressão), prefira comptime.
  • Tagged union tem branch prediction melhor: para poucas estratégias (2-4), um switch sobre a union pode ser mais rápido que ponteiro de função porque o preditor de branch do processador aprende os padrões de acesso.
  • Trocar estratégia em runtime: ponteiros de função e tagged unions permitem trocar a estratégia em runtime sem recompilar. Comptime não permite — a estratégia é fixada em tempo de compilação.

Erros Comuns

Usar ponteiro de função para estratégia que nunca muda: se a estratégia de ordenação é sempre bubbleSort para um determinado tipo de dado, não há razão para usar ponteiro de função. Um comptime parameter ou chamada direta é mais eficiente e mais fácil de entender.

Estratégia com estado compartilhado entre usuários: se a estratégia tem estado interno (contadores, cache), e múltiplos objetos compartilham a mesma instância de estratégia via ponteiro, o estado é compartilhado. Isso pode ser intencional (cache compartilhado) ou um bug sutil (contador duplicado). Documente claramente.

Tagged union sem exaustividade: ao usar else => unreachable em vez de listar todos os casos, você perde a proteção do compilador. Quando adicionar uma nova estratégia à union, o compilador não vai alertar sobre os switch que precisam ser atualizados.

Perguntas Frequentes

Qual das três formas (comptime, ponteiro de função, tagged union) devo escolher? Use comptime quando a estratégia é conhecida em compile time e performance é crítica. Use tagged union quando o conjunto de estratégias é fixo e fechado — você ganha exaustividade verificada pelo compilador. Use ponteiros de função quando o conjunto de estratégias é aberto (plugins, extensões por terceiros) ou quando você precisa de estratégias com estado via anyopaque.

Strategy é o mesmo que injeção de dependência de comportamento? Sim — Strategy é uma forma específica de Dependency Injection onde o que é injetado é um algoritmo ou comportamento, não um serviço. A diferença é principalmente de intenção: DI foca em desacoplar dependências de infraestrutura; Strategy foca em intercambiar algoritmos.

Como comparar performance entre estratégias? Use std.time.Timer para medir o tempo de execução de cada estratégia com dados representativos do seu caso de uso real. Benchmarks artificiais frequentemente favorece a estratégia errada.

Quando Evitar

  • Quando só existe uma estratégia (e não se planeja extensão)
  • Se comptime resolve o problema sem necessidade de troca em runtime
  • Poucas variantes simples — um switch direto pode ser mais claro

Veja Também

  • Factory — Criar a estratégia certa baseada em config
  • Observer — Notificar sobre mudança de estratégia
  • Type Erasure — Interfaces genéricas em runtime
  • Comptime — Strategy resolvido na compilação
  • Enums e Unions — Tagged unions para estratégias

Continue aprendendo Zig

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