Illegal vs Unwanted States
24 points by ajdecon
24 points by ajdecon
This distinction comes up a lot in parsing scenarios. For example, robust interfaces rarely if ever parse integers as unsigned even if negative numbers are not allowed, as that would make it impossible to detect and adequately signal the error to the user. Similarly, compilers have to be able to represent illegal syntax in order to produce good error messages, which introduces a not insignificant amount of complexity to the design.
It's also one of the big reasons prototypes can be hard to iterate into real products, as we have likely already chosen a state representation which makes it easier to implement the happy paths but impossible to handle deviations. Starting from scratch with a more complete state machine in mind is often faster and more reliable.
I've been dealing with this at work lately. We're in the process of finishing a rollout of a rewrite of our service, and a big part of the long tail has been cases where the RPC we were getting had garbage data because some other team had bugs in their own service, the old code happened to do the right thing because of an implementation detail, but the new code was stricter about parsing and caused an error. And the team in question didn't understand their own code well enough to answer why the data was garbage. So we just had to implement a "recovery" mechanism.
Ah, classic case of Postel's law only ever working in one direction (forcing downstream to accept malformed input) and swiftly turning into Hyrum's law.
A bit off topic, but I've been thinking a lot about the relationship between these two rules and Liskov subtyping. One of the rules of subtyping is that preconditions on the input can be weakened and postconditions on the output can be strengthened, or super.pre => sub.pre and sub.post => super.post. This gives us Postel's law. But when we strengthen postconditions, we also break predicates that are coincidentally true. For example, given:
super.post = "id" in out.keys
sub.post = "id" in out.keys && "time" in out.keys
coincidence = "time" not in out.keys
Then sub.post => super.post, but super.post && coincidence could be true while sub.post && coincidence is always false. This gives us Hyrum's law.
Yep. To make things worse, there are some kinds of nonsensical requests that could actually be the end user's fault, so we're having to distinguish
At least we have really good local replay tools.
I'd add a significant category to unwanted states: messy reality.
This comes up a lot with people data, which is often incorrect, but still... that's the state. I once worked on an insurance system where many legacy paper documents had errors - totally invalid dates, addresses that didn't geocode. But they were legal documents -- it was a reality we needed to capture. Systems that could not hold unwanted states + rejected the data outright were a real bane.
This reminds me of a UI design thing: a form that submits a sum type (to some API) should use a product type for its UI state.
For example:
Form to Compute Thing
...
[x] Store the output:
(x) At a default location
( ) At a custom location: [__/uri/or/path/or/something_____]
The API can be nice and restrictive:
struct Request {
...
output: Optional<Location>,
}
enum Location {
Default,
Custom(String),
}
But the UI wants to preserve the textbox state (grayed out) when you're in the None or Default state. The UI state should be a product type:
struct Form {
checkbox: bool,
radio: enum { Default, Custom },
textbox: String,
}
Pretty sure this was from an Evan Czaplicki talk about Elm, but I can't remember the title.
Some thoughts on this: if your architecture follows a functional core, imperative shell philosophy, the permissive type that can represent unwanted states can be converted at the core boundary to the restrictive type that makes them illegal. If there's no structural difference between the two (i.e. the type checker cannot distinguish them) you can use phantom types to encode the validation proof.
In languages that support exceptions we can think of them as a way of representing illegal states out of band until we reach a point in the code where the (presumable) error can be explicitly represented and handled.