3 pontos por GN⁺ 2026-02-23 | 1 comentários | Compartilhar no WhatsApp
  • Explica uma abordagem de design em Rust que usa o sistema de tipos para garantir invariantes em tempo de compilação, em vez de validação em tempo de execução
  • Define novos tipos (newtype) como NonZeroF32 e NonEmptyVec para tornar estados inválidos (0, vetor vazio etc.) impossíveis de representar
  • Em vez de retornar falha com Option ou Result, reforça as restrições nos argumentos das funções para bloquear erros antecipadamente
  • Apresenta casos como String::from_utf8 e serde_json::from_str, que convertem por meio de parsing para tipos com significado
  • O princípio de design de tornar estados ilegais impossíveis de representar e adiantar a validação o máximo possível melhora a estabilidade e a legibilidade do código

1. Expressar restrições com tipos em vez de validação em tempo de execução

  • Na função divide(a, b), dividir por 0 causa pânico em tempo de execução
    • É possível expressar a falha retornando Option, mas isso enfraquece o tipo de retorno
  • Define-se o tipo NonZeroF32 para permitir a criação apenas de valores diferentes de 0
    • O construtor tem a forma fn new(n: f32) -> Option<NonZeroF32>, retornando None em caso de falha
    • Se for definido como divide_floats(a: f32, b: NonZeroF32), a validação em tempo de execução deixa de ser necessária
  • A responsabilidade pela validação é movida de dentro da função para o lado do chamador, eliminando erros antecipadamente

2. Remoção de validação duplicada e simplificação do código

  • Na função roots(a, b, c), tratar a validação de a == 0 com Option gera validação duplicada tanto no chamador quanto na própria função
  • Com NonZeroF32, a validação é feita apenas uma vez, e a lógica posterior fica mais simples
  • Pelo mesmo princípio, define-se NonEmptyVec<T> para não permitir vetores vazios
    • Se get_cfg_dirs() retornar NonEmptyVec<PathBuf>, não será necessária validação adicional depois em main()

3. Casos reais: String e serde_json

  • String é internamente um novo tipo (newtype) de Vec<u8>, e String::from_utf8 faz a verificação de validade
    • Depois disso, pode ser usado com segurança como uma string com UTF-8 garantido
  • serde_json em from_str::<Sample> faz o parsing do JSON para uma struct e garante em tempo de compilação a existência dos campos e a consistência dos tipos
    • Todas as restrições, como existência dos campos foo e bar, correspondência de tipos e tamanho de arrays, são verificadas no nível de tipos

4. Dois princípios do design orientado por tipos

  • Tornar estados ilegais impossíveis de representar
    • NonZeroF32 não pode representar 0, e NonEmptyVec não pode representar o estado vazio
    • Funções simples de validação como is_nonzero continuam permitindo representar estados inválidos, então são incompletas
  • Fazer a validação o mais cedo possível
    • Quando a validação fica espalhada pelo código, como em “Shotgun Parsing”, isso pode levar a vulnerabilidades de segurança (como CVE-2016-0752)
    • Se todas as restrições forem verificadas na etapa de parsing, a lógica posterior pode ser executada com segurança

5. Provas baseadas em tipos e aplicações em Rust

  • Pela correspondência de Curry-Howard, tipos podem ser vistos como proposições lógicas, e valores como suas provas
    • Com a crate typenum, é possível verificar relações matemáticas (3 + 4 = 8) em tempo de compilação
  • O sistema de tipos pode ser usado para provar a correção do programa no momento da compilação

6. Conselhos para aplicação prática

  • Mesmo que uma API externa exija tipos simples (bool, i32), internamente vale a pena representar isso com enums ou newtypes significativos
    • Ex.: definir LightBulbState { On, Off } e implementar From<LightBulbState> for bool
  • Se houver funções de validação simples como verify() ou do_something_fallible(), vale considerar uma conversão estruturada de tipos por meio de parsing
  • Em funções sem efeitos colaterais, é possível usar algo como Result<Infallible, MyError> para representar intencionalmente estados impossíveis no tipo

7. Conclusão

  • Ao usar o sistema de tipos de Rust como ferramenta de validação, a clareza e a estabilidade do código melhoram
  • Várias ferramentas do ecossistema Rust, como Vec, sqlx e bon, já fazem uso de design baseado em tipos
  • Nem todo problema pode ser resolvido com tipos, mas a abordagem de elevar a lógica de validação para o nível dos tipos melhora a manutenibilidade e a segurança
  • Recomenda-se aproveitar ao máximo o poderoso sistema de tipos de Rust para escrever código em que o compilador capture os erros

1 comentários

 
GN⁺ 2026-02-23
Opiniões do Hacker News
  • O exemplo de divisão por zero usado neste texto não é adequado para explicar o princípio “Parse, Don’t Validate”
    O núcleo desse princípio está em funções que convertem dados não confiáveis em tipos estruturalmente corretos
    No texto de Alexis King, "Names are not type safety", também se afirma que o padrão newtype não garante um “correct by construction” completo
    Quando o sistema de tipos não consegue expressar diretamente os invariantes, uma abordagem realista é usar tipos abstratos que imitam um parser por meio de um smart constructor
    O segundo exemplo, de um non-empty vec, é um caso muito melhor, pois garante dentro do sistema de tipos que “sempre existe pelo menos um elemento”

    • Mesmo assim, o “parse, don’t validate” baseado em newtype é muito útil na prática
      Quando não se sabe de onde veio uma string, um valor encapsulado aumenta bastante a confiabilidade
      Para um correctness-by-construction completo, seria necessário um sistema de tipos dependentes, mas também existem alternativas mais leves, como os pattern types do Rust
      Por exemplo, é possível limitar um intervalo como em i8 is 0..100 ou representar slices não vazios com [T] is [_, ..]
      Ainda assim, uma non-empty list no formato (T, Vec<T>) mostra o conflito entre praticidade e pureza teórica, porque tem muitas limitações para ser tratada como um vetor
    • ‘correct by construction’ é o objetivo final
      Tipos como NonZeroU32 são simples, mas a verdadeira força está em projetar toda a lógica de domínio como tipos, fazendo o compilador atuar como guardião
      Com isso, o peso da depuração sai do runtime e vai para o momento do design
    • Também é possível encontrar materiais relacionados pela expressão “make invalid states impossible/unrepresentable”
      Como exemplos, vale consultar "Domain Modeling Made Functional" e o vídeo relacionado
    • O exemplo de divisão por zero é um caso ruim de separação de responsabilidades
      Em vez de tentar encapsular algo nesse nível, a diferença ficaria mais clara ao envolver o comportamento de funções aritméticas, como no caso de overflow
  • Foram reunidos links de discussões recentes relacionadas
    Parse, Don't Validate (2019) (fevereiro de 2026, 172 comentários)
    Parse, Don’t Validate – Some C Safety Tips (julho de 2025, 73 comentários)
    Parse, Don't Validate (2019) (julho de 2024, 102 comentários) etc.
    Foi compartilhado apenas como referência

  • A abordagem de parsing em vez de validação tem limites quando não é possível conhecer todos os casos do mundo real
    Fazer com que algo falhe o mais cedo possível, como em formatos de arquivo, é ótimo, mas é preciso cautela ao aplicar isso em lógica de negócio ou na modelagem de transições de estado
    Se as exigências do mundo real mudarem, o sistema pode deixar de acomodá-las, e no fim os usuários acabam contornando-o

  • Em outras linguagens, é possível ir além com tipos dependentes
    Por exemplo, get_elem_at_index(array, index) pode garantir em tempo de compilação que o índice está dentro do intervalo, mesmo sem conhecer previamente o tamanho do array
    Os tipos Vect n a e Fin n de Idris são exemplos disso

    • No Rust também existem bibliotecas baseadas em macro que imitam tipos dependentes
      Ex.: anodized (vídeo de apresentação)
    • Se o tamanho do array for lido de stdin, não será possível conhecê-lo em tempo de compilação, então esse tipo de verificação fica limitado aos casos com informação estática
    • É desejável que esse tipo de recurso se torne mais comum
  • Também existe a abordagem de colocar várias funções sobre um único tipo
    É a forma de linguagens como Clojure, em que um único map representa todos os dados, e toda a biblioteca padrão pode manipulá-lo

    • Há uma tensão entre a frase de Perlis, “100 funções para uma estrutura de dados”, e “Parse, Don’t Validate”
      É possível colocar invariantes importantes no tipo, ou expressá-los por meio de funções simples
      Mesmo em linguagens de tipagem dinâmica, há hábitos de design que produzem efeito semelhante
    • Isso não é tanto uma alternativa pura, mas sim um trade-off
      Entradas externas ainda precisam ser parseadas no fim das contas, então isso não substitui tudo por completo
    • Soa parecido com a crítica a “stringly typed language”, mas na prática trata-se de um processo de refinar gradualmente a forma dos dados
    • O equilíbrio é importante
      Em sistemas de tipos estruturais, dá para imitar tipos nominais com branding, e o inverso também é possível, mas não é ergonômico
      No fim, o mais realista é combinar adequadamente as duas abordagens
  • Essa discussão lembra o recurso de concepts do C++
    Em Concept-based Generic Programming, de Bjarne Stroustrup, aparece um exemplo que valida automaticamente conversões inteiras
    Tipos como Number<unsigned int> ou Number<char> lançam exceção quando o valor sai do intervalo

  • O exemplo try_roots do texto é, na verdade, um contraexemplo
    Expressar no tipo a restrição b^2 - 4ac >= 0 fica muito complexo no Rust
    Nesse tipo de caso, faz mais sentido simplesmente retornar Option e validar dentro da função
    A maior parte das validações lida com a interação entre vários valores, então resolvê-las por “parsing” acaba sendo inconveniente

    • Quando a validade da entrada depende da relação entre vários argumentos, no fim é preciso juntá-los em algo como fn(abc: ValidABC)
  • Esse padrão também se encaixa bem em design de API
    Em vez de validar uma requisição JSON, se ela for parseada desde o início para uma struct com garantias de tipo, a lógica posterior não precisa repetir validações
    Isso é fácil de implementar em Rust com a combinação de serde + custom deserializer
    Houve quem visse casos reais em que esse método reduziu em 60% o código de tratamento de erros

    • Em Go também se tenta isso, mas o resultado fica um pouco verboso por causa do uso excessivo de ponteiros e da ausência de tipos algébricos
  • A mesma filosofia também é aplicada a sistemas de design de UI
    Em vez de verificar CSS depois, define-se um tipo que só permite posicionamento em unidades de grade, fazendo com que margens arbitrárias como 13px virem erro de compilação
    Assim, o design permanece determinístico

    • Houve uma pergunta sobre quais ferramentas são usadas
  • records + pattern matching de C# se aproximam dessa abordagem
    As discriminated unions de F# são ainda mais poderosas, permitindo tornar estados inválidos impossíveis de representar com Result<'T,'Error>
    Se o C# receber DU nativa no futuro, isso ficará muito mais elegante