Back
Rusty thoughts on Parse, don't validate: invariants become types
SiTech AI Team3 წთ. საკითხავი

Rusty thoughts on Parse, don't validate: invariants become types

Eli Bendersky revisits Alexis King's 'Parse, don't validate' for Rust: instead of checking a condition and trusting it, turn data into a type whose invariant cannot be broken.

Eli Bendersky has published an essay on his blog that revisits an idea from Alexis King's 2019 article "Parse, don't validate". King's original post, written for Haskell, argues that code should not merely check a condition and then trust it, but convert incoming data into a type that makes the guaranteed property impossible to violate. Bendersky applies the same lens to Rust and looks for teachable examples in the language's standard library and in well-known open-source projects.

A vector that cannot be empty

The starting point is the ordinary Vec and its first() method, which returns an optional reference because a vector may hold no elements at all. Bendersky's example reads a list of configuration directories from the CONFIG_DIRS environment variable and rejects an empty result, yet the caller must still write a branch for a case that can never happen. He calls such invariants a ticking time bomb: the compiler does not enforce them, so they can quietly break later. The fix is a dedicated type, such as NonEmpty from the nonempty crate, which offers no constructor for an empty value and returns a plain reference from first(). Returning a checked Vec is validation, the post argues; returning a NonEmpty is parsing, if parsing is understood broadly as transforming data from one format into another.

Pipelines, paths and gradual refinement

The essay then leaves toy code behind. posixutils-rs, the Rust rewrite of core POSIX utilities, stores the commands of a shell pipeline as a NonEmpty, so a parsed pipeline always holds at least one command, and the parser returns None only when there is nothing to run. rust-analyzer has a type of its own for an absolute filesystem path: the conversion fails if the path is not absolute, and later code relies on the type instead of re-checking the condition. Bendersky calls the layering gradual parsing: a path buffer is first proven to be valid UTF-8 by the camino crate, and only then proven absolute.

Non-zero numbers and JSON

Rust's standard library has a generic NonZero type for unsigned numbers that are known not to be zero. The available_parallelism function returns one on success, and division with a NonZero denominator is documented as an operation that cannot panic. There is an extra benefit: zero is an invalid bit pattern for the type, so an optional NonZero takes up exactly the same space as a plain integer. The same idea shows up in JSON parsing, where serde lets a configuration struct declare a non-zero worker count and an enum with two allowed modes; deserialization then enforces field types, permitted enum values and the non-zero rule at once. In dynamic languages the process is far more manual, Bendersky notes: Python's json.loads returns a dictionary and leaves validation to the caller, while libraries such as Pydantic come closer to the Rust approach but are not universally used.

SSiTech

SiTech — AI-powered web development

We build fast, modern websites and bring AI into real business workflows. Have a project or a question? We'd love to help.