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
switchsobre 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
switchdireto 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