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
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.
Yes, it'd probably be compiled into very machine code. However defining an enum for it puts the intention into the source code more explicitly, so that's considered the more idiomatic way. You'd probably have a `fn peer_id(&self) -> Option<Inet4Addr>` accessor that you'd use in the case where you'd want the Option.
Personally I don’t like using nil to represent specific states in my domain. I try to only judiciously use it to represent the lack of knowledge about some state, like if I need to reach out to some external service I don’t control, that may or may not give me an answer for my query (and conversely, I don’t like putting “unknown” cases into my enums).
Yes, the type I wrote down is isomorphic to Option<int32>, so the compiler will ultimately generate the same machine code and do the same optimizations.
But defining a new type allows you to give the two cases more descriptive names. That saves you from having to remember and comment what the two cases represent. In a small but significant way, the code documents itself.
Also, doing it this way gives the Connection type its own name and identity separate from Option<int32>. So if you have a function that takes a Connection and some other Option<int32>, you can’t accidentally switch them: even though the two types are equivalent for optimizations and codegen, they’re NOT equivalent for the type checker.
It also makes function signatures more self-documenting: you can tell just from the type that the function takes or returns a Connection, and that helps in a small but real way to understand what it does.
I like programming like this a lot! So much so that the reason I personally like Rust a lot is simply that it’s a mainstream language with great tooling that has sum types and reasonable generics. It’s like if OCaml or Haskell had the community or tooling, or if Go or Python had the type features.
articulatepang · · focus · HN ↗
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
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 Now we could have connected = true but peer_ip = None.The solution is to use a sum type:
(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.
laszlokorte · · focus · HN ↗
slopinthebag · · focus · HN ↗
sendfoods · · focus · HN ↗
None = disconnected
treyd · · focus · HN ↗
mckn1ght · · focus · HN ↗
slopinthebag · · focus · HN ↗
atoav · · focus · HN ↗
In some cases using None that way is totally fine, but it is a trade off and the cleaner way is to use a separate type
articulatepang · · focus · HN ↗
But defining a new type allows you to give the two cases more descriptive names. That saves you from having to remember and comment what the two cases represent. In a small but significant way, the code documents itself.
Also, doing it this way gives the Connection type its own name and identity separate from Option<int32>. So if you have a function that takes a Connection and some other Option<int32>, you can’t accidentally switch them: even though the two types are equivalent for optimizations and codegen, they’re NOT equivalent for the type checker.
It also makes function signatures more self-documenting: you can tell just from the type that the function takes or returns a Connection, and that helps in a small but real way to understand what it does.
I like programming like this a lot! So much so that the reason I personally like Rust a lot is simply that it’s a mainstream language with great tooling that has sum types and reasonable generics. It’s like if OCaml or Haskell had the community or tooling, or if Go or Python had the type features.