- 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
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
newtypenão garante um “correct by construction” completoQuando 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”
newtypeé muito útil na práticaQuando 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..100ou 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 vetorTipos como
NonZeroU32sã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ãoCom isso, o peso da depuração sai do runtime e vai para o momento do design
Como exemplos, vale consultar "Domain Modeling Made Functional" e o vídeo relacionado
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 arrayOs tipos
Vect n aeFin nde Idris são exemplos dissoEx.: anodized (vídeo de apresentação)
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
É 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
Entradas externas ainda precisam ser parseadas no fim das contas, então isso não substitui tudo por completo
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>ouNumber<char>lançam exceção quando o valor sai do intervaloO exemplo
try_rootsdo texto é, na verdade, um contraexemploExpressar no tipo a restrição
b^2 - 4ac >= 0fica muito complexo no RustNesse tipo de caso, faz mais sentido simplesmente retornar
Optione validar dentro da funçãoA maior parte das validações lida com a interação entre vários valores, então resolvê-las por “parsing” acaba sendo inconveniente
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
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
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