logoalt Hacker News

Rusty thoughts on "Parse, don't validate"

49 points • by ingve • today at 8:59 AM • 21 comments • view on HN

Comments

articulatepang • today at 5:37 PM

I prefer a slightly more general rule: Make Illegal States Unrepresentable. The "Parse, don't validate" rule is a special case of MISU.

What's the difference? MISU applies even when there's no parsing-like transformation happening. For example, if you have a variable that represents the current state of a network connection, and let's say it can be Disconnected, or Connected to some IP address (this is an oversimplification).

Then one way to do it would be

  struct {
    connected: bool,
    peer_ip: int32
  }
The trouble is that this allows us to represent an illegal/meaningless state: we're disconnected but there's still some junk old peer_ip hanging in there. Even worse, we might have written

  struct {
    connected: bool,
    peer_ip: Option<int32>
  }
Now we could have connected = true but peer_ip = None.

The solution is to use a sum type:

  type connection = 
     Disconnected
   | Connected of int32
(sorry for using made-up syntax; I hope it's clear to anyone familiar with Rust.)

"Make Illegal States Unrepresentable" applies throughout your program, at every interface between modules or functions in the program, including but not limited to parsing input.

➕ show 1 reply
Fluorescence • today at 3:49 PM

Not sure that type is good advice:

    pub struct NonEmpty<T> {
        pub head: T,
        pub tail: Vec<T>,
    }
You'd have to manually implement the traits to support the ergonomics of slices and iteration and costly reallocation if you need to pass ownership as a Vec:

I'd expect:

    pub struct NonEmpty<T> {
        v: Vec<T>,
    }
The constructor would enforce the invariant and then you'd impl Deref and DerefMut for [T] to gain normal len/is_empty/indexing/iteration, passing as &[T] to other funcs and mutating values (which can't break the invariant).

To mutate length while preserving the invariant it's dealers choice e.g.

- add .into_vec() for unwrap/mutate/rewrap

- add invariant preserving mutators of your choice

➕ show 3 replies
jelder • today at 2:38 PM

This is great. Alexis King actually stated that, had she known how popular “Parse, Don’t Validate” had been, she would have written it in a language more widely used than Haskell.

➕ show 1 reply
jph • today at 2:06 PM

Good article on Rust's strengths with types. If you like this, you may be curious how you might build your own parse capabilities. I like the Rust crates Winnow and Nom, and also the Rust traits From and Into.

the__alchemist • today at 3:07 PM

Another take, from the primary example: This is what `unwrap()` is for. I understand that the author is looking at this from a correctness and safety(?) perspective. For practical purposes, I would unwrap here. If it's less trivial than the example, unwrap with a comment explaining why it's fine.

Another angle: Unfortunately, the `first()` method being fallible here is just an issue of using an imperfect method/datatype here. This is where the author gets in to a non-empty-vec custom type. Then you are balancing using a more correct type that takes custom wiring vs a std lib thing everyone understands and takes no setup. I would lean towards this setup if I were using this non_empty_vec.first() unwrap pattern a number of times in the code base; then the setup would be worth it, at least for my own code bases. If I were exposing this in a lib others would use, I would keep the standard Vec so as to be more transparent for others.

In both views: "This is what unwrap is for" does it for me in all cases I've encountered to date. Maybe for aerospace or safety critical systems, I would have a different take.

A third take: I notice this trend in the rust community. It's not my cup of tea. Keep things simple, easy to maintain, and don't let "correctness" get in the way. In this example, I don't think it gets in the way, but I have seen this mindset lead to it getting in the way, especially in embedded, where mapping the Owernership model to hardware ends up in messy patterns and surprising assertions about embedded-101 concepts like DMA being "unsolved", "no good way", "difficult" etc.

Rust provides tools to make sure specific logic is correct if it passes the compiler. People sometimes go overboard and assume you have to type-maxx your code, regardless of complexity added by doing so.

tjadfsaj • today at 2:32 PM

Nonempty type wrappers are a stark reminder that we are missing out on refinement types.

victorpudeyev • today at 3:48 PM

...I'm so happy that I write in untyped languages so I don't have to deal with any of this.

➕ show 3 replies