O que são máquinas de estado em Rust
Uma máquina de estados representa um objeto que pode passar por etapas bem definidas. Uma conexão, por exemplo, pode estar fechada, aberta ou encerrada, e cada etapa permite operações diferentes.
Em muitos programas, essa regra fica apenas em comentários ou em verificações espalhadas pelo código. O padrão Typestate leva parte dessa regra para os tipos, permitindo que o compilador impeça chamadas incompatíveis antes do programa rodar.
O padrão Newtype complementa a ideia ao criar um tipo novo ao redor de um valor existente. O tema ganhou atenção entre desenvolvedores Rust porque combina segurança de tipos, APIs mais expressivas e pouca dependência de validações em tempo de execução.
Como funciona
A ideia central é representar cada estado com um tipo separado. Uma estrutura Conexão<Fechada> não é a mesma coisa que Conexão<Aberta>, mesmo que ambas armazenem dados parecidos.
Uma função pode consumir um valor em um estado e devolver o mesmo recurso em outro. Ao abrir uma conexão, por exemplo, abrir recebe uma conexão fechada e retorna uma conexão aberta. O método disponível muda junto com o tipo.
O compilador verifica a cadeia de operações. Se o código tentar enviar dados enquanto a conexão ainda está fechada, a chamada não encontra um método válido para aquele tipo e falha na compilação.
Principais recursos
Typestate e Newtype não são bibliotecas independentes. São padrões que usam recursos nativos da linguagem Rust para transformar regras de domínio em contratos verificáveis.
- Estados explícitos: cada etapa recebe um tipo próprio e torna a API mais fácil de ler.
- Transições controladas: funções podem consumir um estado e devolver outro, evitando combinações inválidas.
- Newtypes: valores parecidos, como metros e segundos, deixam de ser misturados por acidente.
- Custo previsível: muitas verificações são resolvidas na compilação, sem criar uma camada obrigatória de validações dinâmicas.
O diferencial está na composição. O código continua usando structs, enums, traits e generics, mas a interface passa a expressar o que é permitido em cada momento.
Como começar: instalação e acesso passo a passo
O caminho mais simples é instalar o toolchain oficial do Rust com o rustup. Ele instala o compilador, o Cargo e as ferramentas necessárias para criar um projeto local.
Depois, crie uma aplicação pequena para testar uma transição de estado. Um exemplo reduzido é suficiente para perceber a diferença entre uma API que aceita tudo e outra que só expõe operações válidas.
rustup update
cargo new estados-rust
cd estados-rust
cargo runNão é necessário contratar um plano ou serviço externo. Para experimentar no navegador, use o Rust Playground e, para um projeto local, mantenha o toolchain atualizado com o rustup.
Comece modelando uma única transição de negócio. Se a abstração exigir muitos tipos antes de resolver um problema real, reduza o exemplo.
Exemplo prático
Considere um documento que só pode ser enviado depois de validado. O tipo Rascunho representa o documento antes da validação, enquanto Pronto representa o estado que pode ser enviado.
A função validar consome o rascunho e devolve um documento pronto. Como o método enviar existe apenas em Documento<Pronto>, a ordem correta fica visível na assinatura.
struct Rascunho;
struct Pronto;
struct Documento<Estado> {
titulo: String,
estado: std::marker::PhantomData<Estado>,
}
impl Documento<Rascunho> {
fn validar(self) -> Documento<Pronto> {
Documento {
titulo: self.titulo,
estado: std::marker::PhantomData,
}
}
}
impl Documento<Pronto> {
fn enviar(self) {
println!("Enviando: {}", self.titulo);
}
}O exemplo é propositalmente pequeno. Em uma aplicação real, a validação pode verificar campos, permissões ou regras de negócio, enquanto o tipo impede que uma etapa posterior seja chamada cedo demais.
Comparação com alternativas
Uma alternativa comum é manter um campo status em uma única struct e usar match ou if em cada operação. Essa solução é simples e pode ser a melhor escolha quando os estados mudam livremente.
Enums são úteis quando os estados precisam carregar dados diferentes ou quando todas as transições ficam centralizadas em um lugar. Eles tornam a combinação de casos explícita, mas podem concentrar bastante lógica em uma estrutura.
Typestate é interessante quando uma operação deve ser impossível em determinado estado e essa garantia vale em muitos pontos da aplicação. A escolha não é uma disputa entre padrões: use a forma que deixa a regra mais clara para o time.
Pontos positivos e limitações
O principal benefício é detectar erros de uso durante a compilação. Isso reduz a quantidade de caminhos inválidos que precisam ser tratados em produção e transforma documentação implícita em uma interface verificável.
Outra vantagem é a clareza de algumas APIs. Quando uma função recebe Documento<Pronto>, o requisito aparece no tipo, sem depender de uma frase em comentário ou de uma convenção informal.
A limitação é o aumento da complexidade estrutural. Muitos estados, generics e traits podem deixar mensagens de erro difíceis para quem ainda não conhece Rust. Também não faz sentido transformar toda flag em um tipo novo.
O compilador garante as transições representadas pelos tipos, mas não confirma sozinho que a regra de negócio está correta. Os testes continuam necessários.
Casos de uso reais
Bibliotecas de protocolo podem usar Typestate para separar uma conexão configurada de uma conexão pronta para transmitir. A API então reduz o risco de chamar uma operação antes da negociação necessária.
Clientes de armazenamento podem representar uma transação aberta e uma transação finalizada com tipos diferentes. Isso ajuda a evitar que o código tente confirmar duas vezes ou use uma transação depois do encerramento.
Domínios com unidades e permissões também se beneficiam de Newtype. Tipos como IdUsuario, IdPedido e TokenSessao podem envolver strings, mas deixam de ser intercambiáveis por acidente.
Em código de infraestrutura, a abordagem pode representar recursos inicializados em etapas. Um objeto só fica disponível para uma operação sensível depois que passou por configuração, autenticação ou validação.
Dicas e boas práticas
Modele apenas estados que tenham uma consequência prática na API. Se dois estados aceitam exatamente as mesmas operações e não carregam regras diferentes, talvez uma única struct seja mais simples.
Use nomes de tipos que descrevam o domínio, como Rascunho, Validado e Publicado. Eles comunicam mais do que tipos genéricos como StateA.
Prefira funções de transição pequenas e teste cada regra importante. Quando a validação for extensa, separe a lógica de negócio da mudança de tipo para manter o código fácil de revisar.
Combine Newtype com construtores controlados quando um valor só puder existir após uma validação. Assim, o restante do sistema recebe um tipo que já carrega essa garantia.
Evite usar Typestate como decoração. Se a equipe não consegue entender a transição ao ler a API, documente o fluxo ou volte para uma solução mais direta.
Vale a pena?
Typestate e Newtype valem a pena quando estados inválidos causam falhas relevantes, quando uma regra aparece em muitos módulos ou quando a API pública precisa orientar o uso correto.
Para um script curto, um CRUD simples ou um fluxo que muda de forma muito dinâmica, uma enum ou uma struct com validação pode ser mais adequada. A segurança extra precisa compensar a complexidade adicionada.
O próximo passo é escolher um fluxo pequeno do seu projeto, desenhar as transições e implementar apenas uma delas. Se o resultado tornar o código mais claro, expanda o padrão gradualmente.
Não confunda uma API que compila com um sistema correto. Regras externas, dados de entrada, concorrência e falhas de rede ainda precisam de tratamento e testes.