Máquinas de estado são um conceito fundamental em programação, usadas para modelar sistemas que podem estar em diferentes estados e que reagem a eventos realizando transições. Em linguagens tradicionais, o estado é frequentemente representado por uma variável (como um enum ou inteiro) e as transições são verificadas em tempo de execução, o que pode levar a erros se não forem cuidadosamente validadas. Rust oferece uma alternativa poderosa: o padrão Typestate, onde o estado é representado pelo tipo da própria variável. Isso move a validação para o tempo de compilação, eliminando uma classe inteira de bugs.

Nesta aula, vamos mergulhar nesse padrão, entendendo como ele funciona, por que é valioso e como implementá-lo em Rust. Você verá exemplos práticos que demonstram a segurança e a clareza que ele proporciona, além de exercícios para fixar o conceito.

Typestate pattern

O padrão Typestate (também conhecido como "state machine in types") é uma técnica de design em linguagens com sistemas de tipos estáticos avançados, como Rust, onde o estado de um objeto é codificado em seu tipo. Em vez de ter um campo que armazena o estado atual (por exemplo, um enum), o tipo da própria variável indica em qual estado ela se encontra. As operações que fazem transições de estado são funções que consomem o valor atual e produzem um novo valor com um tipo diferente, representando o novo estado.

Esse padrão garante que apenas as operações válidas para um determinado estado possam ser chamadas, já que o compilador rejeita qualquer chamada que não corresponda ao tipo esperado. Isso elimina a necessidade de verificações em tempo de execução (como match ou if) para validar transições, tornando o código mais seguro e auto-documentado. Além disso, o código se torna mais expressivo, pois os estados são explícitos nos tipos das funções.

Vamos ver um exemplo simples: uma conexão de rede que pode estar nos estados Desconectada, Conectada e Autenticada. Em uma implementação tradicional, você teria um enum e verificaria se a operação é permitida. Com Typestate, você define tipos separados para cada estado e funções que transformam um tipo no outro.

Estados como tipos

Em Rust, cada estado é representado por um tipo distinto, geralmente uma struct unitária (sem campos) ou um tipo com dados relevantes. Por exemplo, para uma conexão, poderíamos ter:

struct Desconectada;
struct Conectada;
struct Autenticada;

Esses tipos não possuem campos, mas podem ser estendidos para armazenar informações específicas do estado, como endereço ou token de autenticação. A máquina de estados é então modelada por funções que recebem um estado e retornam outro:

fn conectar(desconectada: Desconectada) -> Conectada {
    // lógica de conexão
    Conectada
}

fn autenticar(conectada: Conectada) -> Autenticada {
    // lógica de autenticação
    Autenticada
}

Note que a função conectar só aceita um valor do tipo Desconectada e retorna Conectada. Se você tentar chamá-la com um valor Conectada, o compilador emitirá um erro. Isso impede que você execute uma transição inválida, como autenticar antes de conectar.

O tipo da variável muda conforme as operações são aplicadas, e você precisa usar a variável retornada para prosseguir. Isso cria um fluxo linear de estados, onde o compilador garante que todas as etapas sejam seguidas na ordem correta.

Segurança em compile-time

A principal vantagem do padrão Typestate é a segurança em tempo de compilação. Erros de transição inválida são detectados durante a compilação, não em tempo de execução. Isso reduz drasticamente a possibilidade de bugs que só apareceriam quando o programa fosse executado, especialmente em sistemas críticos como protocolos de rede, drivers de dispositivo ou máquinas de estados em jogos.

Além disso, o código se torna mais robusto porque o compilador força o programador a lidar com todos os estados possíveis. Se você tentar usar um valor em um estado onde a operação não é suportada, o código nem compila. Isso é particularmente útil em APIs públicas, onde os usuários da sua biblioteca não podem erroneamente usar o sistema de forma incorreta.

Outra vantagem é a eliminação de verificações em tempo de execução. Em uma implementação tradicional, você faria algo como:

enum Estado { Desconectada, Conectada, Autenticada }

fn autenticar(estado: Estado) -> Result<Estado, Erro> {
    if let Estado::Conectada = estado {
        // faz autenticação
        Ok(Estado::Autenticada)
    } else {
        Err(Erro::TransicaoInvalida)
    }
}

Com Typestate, essa verificação é desnecessária, pois o tipo garante que você só tem uma Conectada se já passou pela conexão. Isso torna o código mais rápido (sem custo em tempo de execução) e mais simples.

Exemplos

Vamos construir um exemplo mais completo: uma máquina de estados para um processo de login. Teremos os estados Anônimo, Logado e Admin. As transições serão: login (de Anônimo para Logado), promover (de Logado para Admin) e logout (de Logado ou Admin para Anônimo). Implementaremos isso com tipos.

struct Anonimo;
struct Logado(&'static str); // armazena o nome do usuário
struct Admin(&'static str);

impl Anonimo {
    fn login(self, username: &'static str) -> Logado {
        Logado(username)
    }
}

impl Logado {
    fn logout(self) -> Anonimo {
        Anonimo
    }

    fn promover(self) -> Admin {
        let Logado(username) = self;
        Admin(username)
    }
}

impl Admin {
    fn logout(self) -> Anonimo {
        Anonimo
    }
}

fn main() {
    let usuario = Anonimo;
    let usuario = usuario.login("alice");
    // let usuario = usuario.login("bob"); // erro: método `login` não existe para `Logado`
    let usuario = usuario.promover();
    let usuario = usuario.logout();
    // agora está Anonimo novamente
}

Observe que tentar chamar login em um valor Logado resulta em erro de compilação, pois o método não está definido para esse tipo. Isso é exatamente a segurança que queremos.

Outro exemplo comum é uma máquina de estados para um protocolo de comunicação, onde você só pode enviar dados após estabelecer conexão, e só pode fechar após terminar. Com Typestate, isso fica explícito nos tipos.

Também é possível usar o padrão Typestate com enums que carregam dados, desde que os estados sejam tipos distintos. A flexibilidade do Rust permite combinar com traits, generics e outras features para criar máquinas de estados ainda mais expressivas.

Boas práticas e observações finais

Ao usar o padrão Typestate, é importante manter os tipos simples e focados. Cada estado deve ter apenas os dados necessários para aquele estado. Evite colocar métodos que não fazem sentido para o estado, pois isso quebraria a garantia de segurança.

Uma boa prática é usar nomes de métodos que indiquem a transição, como connect, send, close, etc. Isso torna o código legível e auto-documentado. Além disso, considere usar #[must_use] nas funções de transição para forçar o uso do valor retornado, evitando que o programador ignore o novo estado.

O padrão Typestate é uma ferramenta poderosa, mas não é adequado para todos os casos. Se o número de estados for muito grande ou as transições forem complexas, pode ser mais simples usar um enum tradicional com validação em runtime. Avalie os trade-offs para cada situação.

Referências

Exercícios

  1. Defina tipos para uma máquina de estados de um semáforo: Vermelho, Amarelo, Verde. Implemente as transições avancar (Vermelho -> Verde, Verde -> Amarelo, Amarelo -> Vermelho) e parar (Verde -> Amarelo, Amarelo -> Vermelho).
  2. ✓ Resposta:
    struct Vermelho;
    struct Amarelo;
    struct Verde;
    
    impl Vermelho {
        fn avancar(self) -> Verde { Verde }
    }
    
    impl Verde {
        fn avancar(self) -> Amarelo { Amarelo }
        fn parar(self) -> Amarelo { Amarelo }
    }
    
    impl Amarelo {
        fn avancar(self) -> Vermelho { Vermelho }
        fn parar(self) -> Vermelho { Vermelho }
    }
  3. Crie uma máquina de estados para uma porta que pode estar Aberta ou Fechada. Implemente abrir (Fechada -> Aberta) e fechar (Aberta -> Fechada).
  4. ✓ Resposta:
    struct Aberta;
    struct Fechada;
    
    impl Fechada {
        fn abrir(self) -> Aberta { Aberta }
    }
    
    impl Aberta {
        fn fechar(self) -> Fechada { Fechada }
    }
  5. Implemente uma máquina de estados para um processo de pedido: Rascunho, Submetido, Aprovado, Rejeitado. Transições: submeter (Rascunho -> Submetido), aprovar (Submetido -> Aprovado), rejeitar (Submetido -> Rejeitado).
  6. ✓ Resposta:
    struct Rascunho;
    struct Submetido;
    struct Aprovado;
    struct Rejeitado;
    
    impl Rascunho {
        fn submeter(self) -> Submetido { Submetido }
    }
    
    impl Submetido {
        fn aprovar(self) -> Aprovado { Aprovado }
        fn rejeitar(self) -> Rejeitado { Rejeitado }
    }
  7. Adicione dados aos estados: no exemplo do pedido, faça Rascunho armazenar um título (String), Submetido armazenar título e data, Aprovado armazenar título, data e aprovador. Implemente as transições preservando os dados.
  8. ✓ Resposta:
    struct Rascunho { titulo: String }
    struct Submetido { titulo: String, data: String }
    struct Aprovado { titulo: String, data: String, aprovador: String }
    struct Rejeitado { titulo: String, data: String }
    
    impl Rascunho {
        fn submeter(self) -> Submetido {
            Submetido { titulo: self.titulo, data: String::from("2025-01-01") }
        }
    }
    
    impl Submetido {
        fn aprovar(self, aprovador: String) -> Aprovado {
            Aprovado { titulo: self.titulo, data: self.data, aprovador }
        }
        fn rejeitar(self) -> Rejeitado {
            Rejeitado { titulo: self.titulo, data: self.data }
        }
    }
  9. Escreva um teste que verifica que uma transição inválida não compila. Por exemplo, tente chamar aprovar em um Rascunho e explique o erro.
  10. ✓ Resposta:

    O código let r = Rascunho; r.aprovar(); não compila porque o método aprovar não está definido para o tipo Rascunho. O compilador emite um erro como: error[E0599]: no method named `aprovar` found for struct `Rascunho` in the current scope. Isso demonstra a segurança em tempo de compilação: a transição inválida é impedida antes mesmo de executar.