A type is exactly its set of legal values (its cardinality is that set’s size: bool = 2, u8 = 256, () = 1, an empty enum = 0). Once you see a type as a value-set, data-structure design becomes arithmetic:

  • structs multiply — a struct is a product type, its value-set is the cartesian product of its fields’. struct Pixel { on: bool, shade: u8 } has values, because fields coexist independently.
  • enums add — an enum-with-fields is a sum type, its value-set is the disjoint (tagged) union of its variants’. enum Color { Monochrome, Foreground(RgbColor) } has values, because a value is in one arm or the other.

The design lever: a product over-counts. Its value-set is strictly larger than the domain’s legal states, so it can represent combinations the domain forbids. The tell is a comment policing an invariant the compiler doesn’t:

pub struct DisplayProps {
    pub monochrome: bool,
    // fg_color must be (0,0,0) if monochrome is true.  <-- smell
    pub fg_color: RgbColor,
}

Fix it by choosing the algebra (here, a sum) whose value-set equals the domain’s legal states:

pub enum Color { Monochrome, Foreground(RgbColor) }

Now (monochrome, red) isn’t a value of the type — it has no representation, so it can’t be constructed and there’s nothing to check. That is what “make invalid states inexpressible” means: the invalid region is carved out of the value-set entirely.

Inexpressible ≠ validating constructor. A new() -> Result<Self, E> that rejects bad field combinations is weaker: the invalid value still exists in the type, can be built by bypassing new, and is only caught by a check that runs at runtime. Inexpressibility is a compile-time impossibility — the value simply cannot be typed.

Option and Result are the two universal sums applying this:

  • enum Option<T> { None, Some(T) } — value-set , the “maybe absent” domain carved as a sum.
  • enum Result<T, E> { Ok(T), Err(E) } — value-set , “success or failure”.

This is why a sentinel (-1, nullptr) is the anti-pattern for absence: it signals in-band, jamming “absent” into T’s own value-set so -1 means both “the integer -1” and “no value” — re-creating the over-counting ambiguity. Option pulls “absent” out-of-band into a disjoint None variant, so it can never be confused with real data.

See also

References

Questions

flashcards/rust

Why is a struct called a product type and an enum-with-fields a sum type?::A struct’s value-set is the cartesian product of its fields’ value-sets (multiply — fields coexist independently); an enum’s is the disjoint tagged union of its variants’ (add — a value is in one arm or the other).

What is the essential difference between making a state inexpressible (sum type) and guarding it with a validating constructor (new() -> Result)?::Inexpressible = the bad value has no representation in the type (compile-time impossibility); validating constructor = the bad value still exists and is only rejected by a check that runs at runtime, bypassable if you skip new.

Why is a sentinel value (e.g. -1 meaning “absent”) worse than Option<T>?::The sentinel signals in-band — it overloads a value already inside T’s value-set to mean “absent”, so “absent” isn’t disjoint from real data. Option gives “absent” its own disjoint None variant (out-of-band).