In 2010, Yaron Minsky stood before a room of Harvard undergraduates and listed nine rules for writing effective OCaml. One of them stuck: “Make illegal states unrepresentable.”
That single sentence has shaped how an entire generation of programmers thinks about types. It has been expanded by Scott Wlaschin for F#, reframed as “Parse, don’t validate” by Alexis King for Haskell, and internalized by the Rust community as a core design principle.
But what does it actually mean? And why does it matter beyond style preference?
This series goes deeper than the usual “use an enum instead of booleans” advice. In this first part, we start with the most common mistake: boolean flags.
The problem: a traffic light with booleans #
Imagine you are modeling a traffic light. The naive approach:
struct TrafficLight {
is_red: bool,
is_yellow: bool,
is_green: bool,
}Three booleans. How many states does this type represent?
is_red |
is_yellow |
is_green |
Meaning | Valid? |
|---|---|---|---|---|
false |
false |
false |
All off | No |
true |
false |
false |
Red | Yes |
false |
true |
false |
Yellow | Yes |
false |
false |
true |
Green | Yes |
true |
true |
false |
Red AND Yellow | No |
true |
false |
true |
Red AND Green | No |
false |
true |
true |
Yellow AND Green | No |
true |
true |
true |
All on | No |
Eight combinations, three valid. Five impossible states that the type system happily accepts. A traffic light that is red and green at the same time is not just a bug; it is a safety hazard. Yet the type says “this value can exist.”
The general rule: with n boolean flags modeling mutually exclusive states, you get 2^n representable states but only n valid ones (or n + 1 if “none active” is valid). The remaining 2^n - n values are nonsense the compiler cannot catch.
The fix takes one line #
enum TrafficLight {
Red,
Yellow,
Green,
}Three variants. Three representable states. Three valid states. The ratio is 1:1. A traffic light that is red and green at the same time literally cannot exist in memory. The compiler enforces it, not programmer discipline.
A second example: user sessions #
Consider a user who can be anonymous, logged in, or banned:
// The boolean approach
struct User {
is_logged_in: bool,
is_banned: bool,
}Four combinations. What does is_logged_in: true, is_banned: true mean? Can a banned user be logged in? Should the app show a dashboard or a ban notice? Every function that touches this struct must answer that question, and they might answer differently.
// The enum approach
enum UserStatus {
Anonymous,
LoggedIn { username: String },
Banned { reason: String },
}Three states. Each carries only the data relevant to it. You cannot access username from Banned because that field does not exist on that variant. The type does not just document the invariant. It enforces it.
A third example: online orders #
An online order moves through stages: placed, paid, shipped, delivered, cancelled. The boolean approach:
struct Order {
is_paid: bool,
is_shipped: bool,
is_delivered: bool,
is_cancelled: bool,
}16 combinations (2^4). Only 5 are valid. Can an order be delivered but not shipped? Cancelled but also delivered? The type says yes. The domain says no.
enum OrderStatus {
Placed,
Paid { transaction_id: String },
Shipped { tracking_number: String },
Delivered { delivered_at: DateTime },
Cancelled { reason: String },
}Five variants. Five states. Each carries its own data. A cancelled order does not have a tracking number. A placed order does not have a transaction ID. The impossible combinations are gone.
Why this is not just about style #
There is a distinction between two properties of a type:
- Representational completeness: every valid domain state can be encoded. Both the booleans and the enum achieve this.
- Semantic correctness: only valid domain states can be encoded. Only the enum achieves this.
A product of boolean flags is representationally complete but semantically loose. It admits values that type-check but have no domain meaning. Every such value is a potential bug waiting for a code path to produce it.
Every function that receives the boolean struct must either:
- Ignore the impossible states and hope no code path ever produces them (until it does, during a refactor, at 2 AM in production).
- Defensively check with assertions like
assert!(!(is_paid && is_cancelled)), cluttering the code with guards against states the type system should have prevented.
Neither option is good. The root cause is that the type is too permissive: it can represent values that have no meaning in the domain.
Parse, don’t validate #
Alexis King, in Parse, don’t validate, captures this precisely:
“The difference between validation and parsing lies almost entirely in how information is preserved.”
When you validate with boolean flags, the knowledge (“this order is in the Shipped state”) lives in a runtime value that the type system cannot reason about. When you parse into an enum, the knowledge is encoded in the type, and the compiler can reason about it statically.
A boolean flag is a question you keep asking. An enum variant is an answer you get once.
What comes next #
In Making Invalid States Unrepresentable 2. The algebra behind your types, we look at why this works from a mathematical perspective: algebraic data types, the arithmetic of cardinality, and how the same principle applies across Rust, TypeScript, Java, Scala, Haskell, and OCaml.
In Making Invalid States Unrepresentable 3. Real bugs from representable nonsense, we examine real-world bugs caused by representable nonsense: Tony Hoare’s billion-dollar mistake, the UI state management epidemic, and payment processing nightmares.