State Machine em Zig
Máquinas de estado são uma das implementações mais elegantes em Zig, graças às tagged unions. O compilador garante que todos os estados e transições sejam tratados exaustivamente via switch, eliminando toda uma classe de bugs comuns em máquinas de estado.
Quando Usar
- Protocolos de comunicação (HTTP, WebSocket, TCP)
- Parsing de dados estruturados
- Fluxo de interface (telas, formulários, wizards)
- Game states (menu, jogando, pausado, game over)
- Processos de negócio com estados definidos
Máquina de Estado com Tagged Union
const std = @import("std");
const EstadoConexao = union(enum) {
desconectado,
conectando: struct { tentativas: u8 },
conectado: struct { socket_fd: i32 },
erro: struct { mensagem: []const u8 },
pub fn processar(self: *EstadoConexao, evento: Evento) void {
self.* = switch (self.*) {
.desconectado => switch (evento) {
.conectar => .{ .conectando = .{ .tentativas = 1 } },
else => self.*,
},
.conectando => |info| switch (evento) {
.sucesso => |fd| .{ .conectado = .{ .socket_fd = fd } },
.falha => |msg| blk: {
if (info.tentativas >= 3) {
break :blk EstadoConexao{ .erro = .{ .mensagem = msg } };
}
break :blk EstadoConexao{
.conectando = .{ .tentativas = info.tentativas + 1 },
};
},
else => self.*,
},
.conectado => switch (evento) {
.desconectar => .desconectado,
.falha => |msg| .{ .erro = .{ .mensagem = msg } },
else => self.*,
},
.erro => switch (evento) {
.conectar => .{ .conectando = .{ .tentativas = 1 } },
.resetar => .desconectado,
else => self.*,
},
};
}
pub fn descricao(self: EstadoConexao) []const u8 {
return switch (self) {
.desconectado => "Desconectado",
.conectando => "Conectando...",
.conectado => "Conectado",
.erro => "Erro",
};
}
};
const Evento = union(enum) {
conectar,
desconectar,
sucesso: i32,
falha: []const u8,
resetar,
};
pub fn main() void {
var estado = EstadoConexao.desconectado;
std.debug.print("Estado: {s}\n", .{estado.descricao()});
estado.processar(.conectar);
std.debug.print("Estado: {s}\n", .{estado.descricao()});
estado.processar(.{ .sucesso = 42 });
std.debug.print("Estado: {s}\n", .{estado.descricao()});
estado.processar(.desconectar);
std.debug.print("Estado: {s}\n", .{estado.descricao()});
}
Parser como Máquina de Estado
const std = @import("std");
const ParserCSV = struct {
const Estado = enum {
inicio_campo,
dentro_campo,
dentro_aspas,
aspas_encontrada,
};
estado: Estado = .inicio_campo,
campos: std.ArrayList([]const u8),
inicio_atual: usize = 0,
pub fn init(allocator: std.mem.Allocator) ParserCSV {
return .{
.campos = std.ArrayList([]const u8).init(allocator),
};
}
pub fn deinit(self: *ParserCSV) void {
self.campos.deinit();
}
pub fn parsearLinha(self: *ParserCSV, linha: []const u8) !void {
self.campos.clearRetainingCapacity();
self.estado = .inicio_campo;
self.inicio_atual = 0;
for (linha, 0..) |ch, i| {
switch (self.estado) {
.inicio_campo => switch (ch) {
'"' => {
self.estado = .dentro_aspas;
self.inicio_atual = i + 1;
},
',' => try self.campos.append(linha[self.inicio_atual..i]),
else => self.estado = .dentro_campo,
},
.dentro_campo => switch (ch) {
',' => {
try self.campos.append(linha[self.inicio_atual..i]);
self.inicio_atual = i + 1;
self.estado = .inicio_campo;
},
else => {},
},
.dentro_aspas => switch (ch) {
'"' => self.estado = .aspas_encontrada,
else => {},
},
.aspas_encontrada => switch (ch) {
'"' => self.estado = .dentro_aspas,
',' => {
try self.campos.append(linha[self.inicio_atual .. i - 1]);
self.inicio_atual = i + 1;
self.estado = .inicio_campo;
},
else => self.estado = .dentro_campo,
},
}
}
try self.campos.append(linha[self.inicio_atual..]);
}
};
Máquina de Estado Comptime para Protocolos
Quando os estados e transições são conhecidos em compile time, você pode usar comptime para gerar código de despacho otimizado:
const std = @import("std");
// Tabela de transições resolvida em comptime
fn criarTabelaTransicoes(comptime Estados: type, comptime Eventos: type) type {
const num_estados = @typeInfo(Estados).@"enum".fields.len;
const num_eventos = @typeInfo(Eventos).@"enum".fields.len;
return struct {
tabela: [num_estados][num_eventos]?Estados,
pub fn init() @This() {
var t: @This() = .{ .tabela = .{.{null} ** num_eventos} ** num_estados };
return t;
}
pub fn addTransicao(
self: *@This(),
de: Estados,
evento: Eventos,
para: Estados,
) void {
self.tabela[@intFromEnum(de)][@intFromEnum(evento)] = para;
}
pub fn transicionar(self: *const @This(), estado: Estados, evento: Eventos) ?Estados {
return self.tabela[@intFromEnum(estado)][@intFromEnum(evento)];
}
};
}
Esta abordagem é útil para máquinas de estado com muitos estados e transições, onde o switch aninhado ficaria muito longo.
Considerações de Performance
- Tagged union é a abordagem mais eficiente: o compilador Zig gera um
switchcom jump table para unions com enum tag. Transições de estado são tipicamente uma instrução de load e um jump — custo mínimo. - Estado com dados embutidos: o grande diferencial das tagged unions é poder armazenar dados dentro do estado.
conectando: struct { tentativas: u8 }não precisa de alocação separada — os dados vivem dentro da union. - Enum simples vs tagged union: se os estados não precisam de dados associados, use um
enumsimples comswitch. É mais leve que uma union completa e o compilador pode representar o enum em um único byte se houver menos de 256 valores. - Máquina de estado em hot path: em parsers de alta performance (JSON, HTTP), a máquina de estado é executada para cada byte de entrada. O custo de cada transição deve ser mínimo — prefira
enumsimples comswitcha unions com dados complexos.
Erros Comuns
Não tratar a transição else => self.*: ao adicionar um novo evento ou estado, é fácil esquecer de adicionar as transições correspondentes. Com else => self.*, o compilador não vai alertar sobre casos faltando. Para máquinas críticas, use else => @panic("transição inválida") em debug para detectar estados não tratados.
Estado mutável compartilhado entre transitions: se a função processar chama código externo que pode modificar o estado antes do switch ser avaliado, você pode ter race conditions. Sempre capture o estado atual em uma variável local antes de processar.
Confundir estado e transição: o estado representa onde o sistema está; a transição representa o que aconteceu. Um evento conectar não é um estado — é o evento que causa a transição de desconectado para conectando.
Perguntas Frequentes
Como persistir e restaurar o estado de uma máquina?
Com tagged unions, serialize o tag e os dados do estado ativo. Use std.json.stringify se os dados do estado forem serializáveis, ou serialize manualmente para um formato binário compacto. Na restauração, leia o tag e construa o estado correto.
Posso ter ações executadas na entrada/saída de estados?
Sim — adicione hooks aoEntrar e aoSair que são chamados pela função processar durante a transição. Guarde o estado anterior, execute aoSair no estado anterior, mude o estado, execute aoEntrar no novo estado.
Como debugar qual transição aconteceu?
Adicione logging na função processar: antes e depois da atribuição self.*. Em modo debug, imprima @tagName(estado_anterior), o evento, e @tagName(self.*). Em produção, remova com if (builtin.mode == .Debug).
Quando Evitar
- Lógica com apenas dois estados (basta um
bool) - Quando as transições são lineares e nunca voltam
- Estados que nunca mudam em runtime (use comptime)
Veja Também
- Strategy — Comportamento variável sem estados
- Command — Encapsular transições como objetos
- Enums e Unions — Tagged unions em detalhe
- Error Handling — Estados de erro
- Receitas — Exemplos práticos de parsers