Algebraic Data Types — Interview Q&A¶
Roadmap: Functional Programming → Algebraic Data Types
An ADT is a type built by combining other types two ways: product ("this and that", a struct/record) and sum ("this or that", a tagged union). Pattern matching takes them apart. The payoff is that you can shape the type so that illegal states cannot be written down.
A bank of 45+ questions and answers spanning definitions, domain modeling, the senior moves ("make illegal states unrepresentable", "parse, don't validate", the Expression Problem), and the deep mechanics (type counting, memory layout, match compilation). Examples are in Java, Rust, Python, and Go, with Haskell asides where the pure form clarifies. Use the <details> toggles to self-quiz: read the question, answer out loud, then expand.
Table of Contents¶
- Fundamentals / Junior
- Intermediate / Middle
- Senior — Design with the Type System
- Professional / Deep — Counting, Layout, Compilation
- Code-Reading — Model It / Spot the Impossible State
- Curveballs
- Rapid-Fire / One-Liners
- How to Talk About ADTs in Interviews
- Summary
- Related Topics
Fundamentals / Junior¶
Definitions, the two type constructors, and the everyday
Option/Resultyou already use.
Q1. What is an algebraic data type?
Answer
A type composed from other types using two operations: a **product** (combine fields — "this *and* that") and a **sum** (offer alternatives — "this *or* that"). A `struct`/`record`/`tuple` is a product; an `enum`/tagged union/`Either` is a sum. ADTs are usually paired with **pattern matching**, which destructures a value to recover the fields or discriminate which alternative it is. The "data type" part is ordinary; the "algebraic" part is that these two operations compose like multiplication and addition, which is where the name comes from.Q2. Product type vs sum type — give a concrete example of each.
Answer
A **product** holds *all* of its fields at once: a `Point { x: f64, y: f64 }` is *always* an `x` **and** a `y`. A **sum** holds *exactly one* of its alternatives at a time: a `Shape` is a `Circle` **or** a `Rectangle` **or** a `Triangle`, never two at once. Products are the familiar struct; sums are the type that many mainstream languages historically lacked (Go still does). The two compose: a `Shape` (sum) whose `Circle` variant carries a `radius` and a `center: Point` (product) is a sum of products — the bread and butter of domain modeling.Q3. Why is it called "algebraic"?
Answer
Because the number of distinct values a type can hold follows the algebra of *multiplication* and *addition*. A product type's value count is the **product** of its fields' counts: `(Bool, Bool)` has `2 × 2 = 4` values. A sum type's value count is the **sum** of its variants' counts: `ResultQ4. What is Option / Optional / Maybe, and what problem does it solve?
Answer
It's a sum type with two variants: "a value is present" (`Some(x)` / `Just x`) or "nothing is here" (`None` / `Nothing`). It replaces the **null reference** — Tony Hoare's "billion-dollar mistake" — with an *explicit, type-checked* absence. The difference is that `null` can inhabit *any* reference type silently, so the compiler can't force you to handle it; `OptionQ5. What is Result / Either, and how does it differ from Option?
Answer
`ResultQ6. What is pattern matching and why is it the natural way to consume a sum type?
Answer
Pattern matching inspects a value, branches on *which* variant it is, and **binds** the data inside that variant to names in one step. It's the dual of construction: where a sum is built by *choosing* a variant, it's consumed by *discriminating* the variant. The reason it fits sums so well is **exhaustiveness** — a good compiler checks that you handled every variant and errors (or warns) if you missed one, so adding a new variant later forces you to revisit every match. That compiler-enforced "did you handle all the cases?" is a property `if/else` chains on a type tag simply don't give you.Q7. Show the same Shape sum type in Rust and the closest equivalent in Java.
Answer
enum Shape {
Circle { radius: f64 },
Rect { w: f64, h: f64 },
}
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rect { w, h } => w * h,
}
}
sealed interface Shape permits Circle, Rect {}
record Circle(double radius) implements Shape {}
record Rect(double w, double h) implements Shape {}
double area(Shape s) {
return switch (s) { // exhaustive — compiler checks
case Circle c -> Math.PI * c.radius() * c.radius();
case Rect r -> r.w() * r.h();
};
}
Q8. Is a Java enum an algebraic data type?
Answer
A plain Java `enum` is only the *degenerate* case — a sum type whose variants carry **no data** (`enum Color { RED, GREEN, BLUE }` is the sum `1 + 1 + 1` = three values). True ADTs need variants that *carry different payloads*, which a classic `enum` can't express. In modern Java the ADT is `sealed interface` + `record`s; in Rust and Swift the single `enum` keyword does both jobs because its variants can carry fields. So "enum = ADT" is true in Rust, only partially true in Java.Q9. What's wrong with modeling "a circle or a rectangle" as a struct with nullable fields?
Answer
This is a **product** type (it always has all four fields) pretending to be a **sum** (it should be one shape or the other). It admits nonsense values the domain forbids: a `Shape` with `Kind="circle"` *and* a non-zero `W`, or `Kind="triangle"` (typo) with no matching fields, or all zeros. The type is *wider* than the domain, so every consumer must defensively check "is this combination valid?" A real sum type makes those illegal combinations unrepresentable — you literally cannot construct a `Circle` that also has a width.Q10. What does "exhaustive" matching mean and why do you want it?
Answer
An exhaustive match handles *every* variant of the sum; the compiler verifies there are no gaps. You want it because it converts a whole class of "forgot a case" bugs from runtime surprises into compile errors. The big payoff is **evolution**: when someone adds a `Triangle` variant a year later, every non-exhaustive `match` across the codebase fails to compile, handing them a precise to-do list of places that need the new case. Without exhaustiveness (e.g. a `switch` with a `default` that swallows the unknown), that same change silently routes triangles into the wrong branch.Q11. What is the "unit" type and the "never"/"empty" type, in ADT terms?
Answer
The **unit** type has exactly **one** value (`()` in Rust/Haskell, `Unit` in Kotlin/Scala) — in the algebra it's the number `1`, the identity for `×` (a product with a unit field gains no information: `T × 1 = T`). The **never**/empty type has **zero** values (`!` in Rust, `Void` in Haskell, `Nothing` in Scala) — it's the number `0`, the identity for `+` (`T + 0 = T`). A function returning `!` can never return normally (it loops, panics, or exits), which is why Rust uses it for `panic!` and infinite loops. These are the "0 and 1" that make the type algebra a real semiring.Q12. Do Python and Go have algebraic data types?
Answer
**Products:** both do — Python `dataclass`/`NamedTuple`, Go `struct`. **Sums:** neither has them as a first-class language feature. Python *emulates* sums with `typing.Union` / `X | Y` (3.10+) plus `match` (3.10+), and the idiom of a sealed dataclass hierarchy; the type checker (mypy/pyright) can even enforce exhaustiveness via `assert_never`. Go has **no sum types at all** — the idioms are the `(value, error)` tuple, sealed interfaces (an unexported method so only your package can implement them), or a tagged struct. So: products yes, sums only by convention.Intermediate / Middle¶
Domain modeling, choosing
Optionovernull, recursive ADTs, and the per-language idioms.
Q13. Why prefer Option<T> over a nullable T?
Answer
Three reasons. **Visibility:** `OptionQ14. Walk through modeling a domain with ADTs. Take "a payment method".
Answer
List the *kinds* (that's your sum) and, per kind, the data it needs (each kind's product): The win: a `Card` *cannot* exist without an expiry, and a `Cash` payment *cannot* accidentally carry an IBAN — each variant carries exactly the fields its kind needs and no others. Compare the anti-model (one struct with every field nullable), where `Cash` with a stray CVV is representable and every consumer must guard against it. Modeling with the sum pushes the validation into *construction*, so downstream code reasons about valid data only.Q15. What is a recursive ADT? Give an example.
Answer
A sum type one of whose variants contains the type itself — the shape of trees, lists, and expression grammars. A binary tree:enum Tree {
Leaf(i32),
Node(Box<Tree>, Box<Tree>), // Box = heap pointer, needed for a finite size
}
Q16. How do you model sum types in Go, which has none?
Answer
Three common idioms, each with trade-offs: 1. **`(T, error)` return** — the pervasive built-in "success *or* failure" sum, but limited to that one shape. 2. **Sealed interface** — an interface with an *unexported* marker method, so only types in your package can implement it; consumers type-switch over it: 3. **Tagged struct** — a `Kind` field plus payload fields (the nullable-fields anti-pattern from Q9; sometimes pragmatic for wire formats). The sealed-interface idiom is closest to a real sum, but Go gives you **no exhaustiveness check** — a `switch` over `Shape` needs a `default: panic("unhandled")` and discipline (or a linter like `go-exhaustive`) to catch missing cases. That missing compiler guarantee is the core cost of not having sum types.Q17. How do you model sum types idiomatically in Python?
Answer
A frozen-dataclass hierarchy under a union, matched with `match`: For **exhaustiveness**, add a `case _ as unreachable: assert_never(unreachable)` — `pyright`/`mypy` then error if a new variant is added but not handled. It's enforced by the *type checker*, not the runtime, so a project without static checking gets the structure but not the guarantee.Q18. Option has map and and_then/flatMap. When do you use which?
Answer
Use **`map`** when your transform returns a *plain* value — it stays inside the `Option`: `Some(3).map(|x| x + 1) == Some(4)`. Use **`and_then`/`flatMap`** when your transform *itself* returns an `Option`, so you'd otherwise get a nested `OptionQ19. When should you NOT reach for Option/Result and just throw an exception (or panic)?
Answer
Use the sum type for **expected, recoverable** outcomes the caller should branch on — a missing key, a parse failure, a not-found user. Use exceptions/panics for **truly exceptional, unrecoverable** conditions: programmer bugs (index out of bounds), invariant violations that mean "this should be impossible," or environmental catastrophes you can't sensibly handle locally. The smell is using exceptions for ordinary control flow (an exception for "user not found" on a hot path is both slow and surprising) or using `Result` for genuine bugs (threading an error type through code that can only fail if the program is broken). Match the mechanism to whether the caller can *meaningfully* do something about it.Q20. Compare error handling: Rust Result + ? vs Go if err != nil vs Java checked exceptions.
Answer
All three encode "this can fail," differently. **Rust** makes failure a value (`Result`) and the `?` operator early-returns the error, so the happy path stays linear *and* the compiler forces you to handle or propagate. **Go** also makes failure a value (`error`) but with no propagation sugar, so the `if err != nil { return err }` boilerplate is explicit and repetitive — visible, but noisy. **Java checked exceptions** put failure in the *signature* (`throws IOException`) and force handling, but they're a separate control-flow channel (not a value you can `map`/store) and interact awkwardly with lambdas/streams, which is why much modern Java drifts to unchecked exceptions or `Optional`/`Result`-like types. The ADT approach (Rust/Scala) treats errors as ordinary data you can compose; the exception approach treats them as a side channel.Q21. What combinators exist on Result, and how do you compose a fallible pipeline?
Answer
`map` (transform the `Ok`), `map_err` (transform the `Err`), `and_then` (chain another fallible step), `or_else` (recover/fallback), `unwrap_or`/`unwrap_or_else` (provide a default). A pipeline threads the error automatically: Each step short-circuits on the first error; the success values flow through. This is "railway-oriented programming": one track for success, one for failure, and the combinators keep you from manually checking after every call.Q22. How do Option and Result interconvert, and why would you?
Answer
`Option` → `Result`: attach an error to the `None` case (`opt.ok_or(MyError::NotFound)`), used when crossing into a context where "why is it missing?" matters. `Result` → `Option`: discard the error (`res.ok()`), used when you only care *whether* it succeeded, not why. The conversion direction reflects an information change: going to `Result` you *add* an error reason; going to `Option` you *drop* it. Picking the right one at each boundary keeps each layer carrying exactly the information its callers need — no more, no less.Q23. What's the value of putting an invariant into the type of a variant's field rather than checking it at runtime?
Answer
It moves the check from "every consumer must remember to validate" to "validation happens once, at construction, and the type guarantees it thereafter." If a `Card` variant's number field is `ValidatedCardNumber` (a type you can only obtain by passing validation), then *any* code holding a `Card` knows the number is valid — no defensive re-checks, no "what if it's malformed here?" branches. This is the bridge from ADTs to "parse, don't validate" (Q26): the sum type narrows *which kinds* exist; refined field types narrow *which values* each kind can hold.Senior — Design with the Type System¶
Making illegal states unrepresentable, parse-don't-validate, the Expression Problem, and evolving ADTs.
Q24. Explain "make illegal states unrepresentable." Give a before/after.
Answer
The principle: design types so that values your domain forbids *cannot be constructed*, shifting whole categories of bug from "caught at runtime (maybe)" to "caught at compile time (always)." Before — a connection modeled as a product with optional fields: After — a sum where each state carries exactly its data: Now "connected without a session" and "connected and failed at once" are not bugs to guard against — they're sentences the type system won't let you write. You shrink the type down to exactly the domain.Q25. Relate the algebra of types to "make illegal states unrepresentable."
Answer
The product anti-model in Q24 has `2 (connected) × |OptionQ26. What is "parse, don't validate"?
Answer
A coined principle (Alexis King): instead of *validating* data and passing the same loose type onward (so the next function must validate again), **parse** it once into a *narrower type that encodes the proven facts*. A `validate_email(s: str) -> bool` leaves you holding a `str` you must keep re-trusting; a `parse_email(s: str) -> OptionQ27. "Parse, don't validate" vs ordinary input validation — what actually changes?
Answer
Validation answers "is this OK?" and returns a *boolean or throws*, then hands the **original wide type** downstream — so the knowledge "it's valid" lives only in the programmer's head and must be re-established (or assumed) at every later use. Parsing answers "turn this into the precise thing or fail" and returns a **narrower type** that *carries the proof*. The practical difference: with validation, a function deep in the system that receives a `String` can't tell whether it was validated, so it either re-validates (waste, drift) or trusts (latent bug). With parsing, that function receives an `Email`/`NonEmptyList` and the type *is* the guarantee — no re-checking, no "I hope someone validated this." Same check, but its result is preserved in the type instead of evaporating.Q28. What is the Expression Problem, and how do ADTs and OO class hierarchies sit on opposite sides?
Answer
The Expression Problem (Wadler) asks: can you add **new variants** (data cases) *and* new **operations** over them, **without modifying existing code** and **keeping static type safety**? The two paradigms make opposite trade-offs: - **ADT + pattern matching** makes adding an **operation** easy (write one new function with a `match` over all variants — touch nothing else) but adding a **variant** hard (every existing `match` must gain a case — and the compiler makes you). - **OO class hierarchy** (subtype polymorphism) makes adding a **variant** easy (write a new subclass with all methods — touch no existing class) but adding an **operation** hard (you must add a method to *every* existing class). So the axis you expect to grow picks the tool: operations grow → ADTs; kinds grow → class hierarchy. Neither wins both; patterns like the Visitor (recovers ADT-like dispatch in OO) and tagless-final / typeclasses (recover open extension in FP) are attempts to get both at some cost.Q29. So when should I use a sum type vs a polymorphic class hierarchy?
Answer
Decide by which axis is **open** vs **closed**. If the set of *kinds* is **closed and known** (the four connection states, the JSON node kinds, the AST node types) and you expect to keep *adding operations* over them, a sum type is ideal — closed set means exhaustiveness works for you, not against you. If the set of *kinds* is **open / extensible by third parties** (plugins, drivers, UI widgets others will subclass) and the operations are relatively fixed (`render`, `validate`), a polymorphic interface is better — new kinds slot in without recompiling the world. A telltale: ASTs, protocol messages, and state machines are closed → ADT; plugin and strategy hierarchies are open → polymorphism.Q30. Why does adding an enum variant break existing code — and why is that a good thing?
Answer
Because every exhaustive `match` that didn't have a `default`/wildcard now has an unhandled case, so it fails to compile. That break is **the feature**: it's the compiler enumerating, precisely, every site that must consider the new case before you can ship. The alternative — code that compiles fine and silently mishandles the new variant (drops it, routes it to a wrong branch, returns a default) — is a far worse bug, discovered in production instead of at the keyboard. A wildcard `_ =>` arm *suppresses* this safety to buy convenience; on a closed domain type that's usually a mistake, because it trades a compile error for a runtime surprise on exactly the change most likely to need attention.Q31. How do you evolve / version an ADT in a wire format or public API without breaking consumers?
Answer
The exhaustiveness that helps *within* a codebase becomes a hazard *across* a version boundary, because an old consumer can't match a new variant. Tactics: (1) **reserve an `Unknown`/`Other` variant** in serialized forms so old code can round-trip data it doesn't understand instead of crashing (protobuf does this for unknown fields/enum values); (2) treat **adding a variant as a breaking change** for closed clients and gate it behind a version bump; (3) for the *product* side, adding an optional field is backward-compatible, removing or repurposing one is not. The core tension: closed sums give safety inside one binary but are *not* naturally forward-compatible across independently-deployed binaries, so wire ADTs need an explicit escape hatch.Q32. Sum types over deeply-nested booleans — defend the refactor to a skeptical reviewer.
Answer
Lead with cardinality. Three booleans modeling a workflow's state give `2³ = 8` representable combinations, but the domain may have only 3 legal states — so 5 of the 8 are bugs waiting to be constructed, and every reader must mentally exclude them. Replacing the booleans with a 3-variant enum makes the type's cardinality equal the domain's: the illegal combinations literally cannot be written, the names document intent (`Draft`/`Submitted`/`Approved` beats `isSubmitted && !isApproved`), and matches become exhaustive so future states force a revisit. The trade-off to acknowledge: a touch more upfront type-definition and, in serialization, a migration story (Q31). Worth it whenever the state drives branching logic.Q33. How do generics interact with ADTs — what is a parametric (generic) ADT?
Answer
An ADT can be *parameterized* over a type, so `OptionProfessional / Deep — Counting, Layout, Compilation¶
Type isomorphisms, memory representation, niche optimization, and how a
matchbecomes machine code.
Q34. How many values does (Bool, Bool) have? How many does Maybe Bool (Option<bool>) have?
Answer
`(Bool, Bool)` is a **product**: `2 × 2 = 4` values — `(F,F), (F,T), (T,F), (T,T)`. `OptionQ35. What's a type isomorphism, and can you give one from the algebra?
Answer
Two types are isomorphic when there's a lossless back-and-forth conversion (a bijection) between their values — they carry the same information in different shapes, and the algebra predicts them. Examples: `Option<()> ≅ bool` (both have 2 values); `(A, (B, C)) ≅ ((A, B), C)` (products associate); `Either ≅ Either` up to relabeling (sums commute); and the distributive law `A × (B + C) ≅ (A × B) + (A × C)` — "a pair of an `A` and a (`B` or `C`)" is the same as "(an `A` and a `B`) or (an `A` and a `C`)." Even `(A → B → C) ≅ (A × B → C)` is the curry/uncurry isomorphism, exponentials `C^(A×B) = (C^B)^A`. Recognizing these lets you refactor a type into an equivalent, more convenient shape with confidence you've lost nothing.Q36. How is a sum type laid out in memory, and what is "niche" optimization?
Answer
Naively, a sum is a **tag** (a small integer saying which variant) plus enough space for the **largest** variant's payload (a "tagged union"), so `enum E { A(u8), B(u64) }` costs roughly the tag + 8 bytes, with alignment padding. **Niche optimization** removes the tag when a payload has spare ("niche") bit patterns the compiler can repurpose as the discriminant. The classic case: `Option<&T>` — a reference can never be null, so Rust uses the all-zeros pattern to mean `None` and the actual address to mean `Some`. Result: `Option<&T>` is the *same size* as `&T` (8 bytes, no extra tag), so the `Option` abstraction is **zero-cost** here. The same applies to `OptionQ37. So is Option<T> always free? When does it cost?
Answer
No. It's free when `T` has a *niche* (a forbidden bit pattern) the compiler can steal for the `None` tag — references, `NonZero*`, `Box`, `bool` (which only uses 2 of 256 byte values), enums with unused discriminants. It **costs** when `T` uses its entire bit space, e.g. `OptionQ38. How does a compiler turn a match into efficient code?
Answer
It compiles the discrimination, not a chain of comparisons where it can avoid one. For matching on a sum's tag (or a dense integer range), it typically emits a **jump table** — index by the tag into a table of branch targets, O(1) regardless of variant count — rather than sequential `if tag == 0 … else if tag == 1`. For sparse values it may use a **binary search / decision tree** over the cases, or a hybrid. Nested and overlapping patterns are lowered to a **decision tree** (the classic Maranget algorithm) that tests each scrutinee field at most as many times as needed and shares common tests across arms. Exhaustiveness and reachability (the "unreachable arm" warning) fall out of the same analysis. Net: idiomatic pattern matching is generally as fast as, or faster than, the hand-written conditional you'd otherwise type.Q39. Why can't you have a Tree ADT without indirection (a pointer/box) in a language like Rust or C?
Answer
Because the compiler must know each type's size at compile time, and a directly-embedded recursive type has no finite size: `enum Tree { Node(Tree, Tree) }` would need `size(Tree) = 2 × size(Tree) + tag`, which has no finite solution (it diverges). Indirection breaks the recursion — a `BoxQ40. What does the "function type" correspond to in the algebra of types, and why is the notation B^A?
Answer
A function `A -> B` corresponds to **exponentiation**: it has `|B|^|A|` distinct values (`B^A`), because to define a total function you choose, *independently for each* of the `|A|` inputs, *one* of the `|B|` outputs — that's `|B|` multiplied by itself `|A|` times. So `bool -> bool` has `2² = 4` functions (identity, not, const-true, const-false), and `() -> B ≅ B` (`B^1 = B`). This completes the algebra (`+`, `×`, exponent) and explains the curry isomorphism `C^(A×B) = (C^B)^A` (`A → B → C` ≅ `(A, B) → C`) and `(A+B) → C ≅ (A→C) × (B→C)` — "handle a sum" is "a pair of handlers," which is precisely why a `match` over a sum has one arm per variant.Code-Reading — Model It / Spot the Impossible State¶
You're shown a snippet; model it as an ADT, or find the state the type lets you build but the domain forbids.
Q41. Spot the impossible state this Java class permits, then fix it with an ADT.
class HttpResponse {
int status; // 0 if not set yet
String body; // null while in flight
String errorMessage; // null unless failed
boolean inFlight;
}
Answer
The product type permits nonsense: `inFlight == true` with a non-null `body` and a non-null `errorMessage` simultaneously; a "completed" response with `status == 0`; a success carrying an `errorMessage`. Model the *states* as a sum: Now `body` exists *only* in `Success`, `errorMessage` *only* in `Failed`, and `InFlight` carries neither — the "in flight with a body and an error" combination is unrepresentable, and a `switch` over the three forces every consumer to handle each state.Q42. Model this Go tagged struct as a proper sum (sealed interface) and say what you gained.
type Event struct {
Type string // "click", "scroll", "key"
X, Y int // only for click
DeltaY int // only for scroll
KeyCode int // only for key
}
Answer
type Event interface{ isEvent() }
type Click struct{ X, Y int }; func (Click) isEvent() {}
type Scroll struct{ DeltaY int }; func (Scroll) isEvent() {}
type Key struct{ KeyCode int }; func (Key) isEvent() {}
func handle(e Event) {
switch e := e.(type) {
case Click: /* use e.X, e.Y */
case Scroll: /* use e.DeltaY */
case Key: /* use e.KeyCode */
default: panic("unhandled event") // Go has no exhaustiveness check
}
}
Q43. This Python function re-validates the same data three times. Refactor it to "parse, don't validate".
def handle(raw: str):
if not is_valid_email(raw): raise ValueError
send_welcome(raw) # re-checks raw inside
store(raw) # re-checks raw inside again
Answer
Parse once at the boundary into a type that *proves* validity, then pass that type around:from dataclasses import dataclass
@dataclass(frozen=True)
class Email:
value: str
@staticmethod
def parse(raw: str) -> "Email | None":
return Email(raw) if is_valid_email(raw) else None
def handle(raw: str):
email = Email.parse(raw)
if email is None: raise ValueError
send_welcome(email) # takes Email — no re-check possible or needed
store(email) # same
Q44. Read this Rust and say what the wildcard arm costs you.
enum Cmd { Start, Stop, Pause }
fn label(c: Cmd) -> &'static str {
match c {
Cmd::Start => "start",
_ => "other", // <--
}
}
Answer
The `_ => "other"` arm *defeats exhaustiveness*. Today `Stop` and `Pause` both fall into `"other"` — already probably a bug. Worse, when someone adds `Cmd::Resume` next quarter, this function silently labels it `"other"` and **the compiler says nothing**, because the wildcard absorbs it. Had each variant been listed explicitly, adding `Resume` would break compilation here and at every other match, handing the author a checklist. The fix is to enumerate the variants you mean and reserve `_` only for cases where catch-all is *genuinely* the intent (e.g. matching over an open/`#[non_exhaustive]` type), not as a shortcut on a closed domain enum.Q45. This Java models a card with Optional fields. What's the smell and the ADT fix?
record DiscountCode(
Optional<Double> percentOff,
Optional<Double> amountOff,
Optional<Integer> freeShippingThreshold) {}
Answer
It's a product of three optionals encoding a sum: a discount is *one of* percent-off, amount-off, or free-shipping — but the type permits all-empty (no discount at all), all-three-set (contradictory), or any combination. `2³ = 8` representable shapes for 3 legal ones. Fix with a sealed sum: Exactly one kind, each with its own field, no empty/contradictory states, and a `switch` that must handle all three. The `Optional`-soup is the Java flavor of the nullable-fields anti-pattern from Q9.Curveballs¶
The questions designed to catch glib answers.
Q46. Why is it called "algebraic" — in one breath?
Answer
Because the count of values follows ordinary algebra: a **product** type's values **multiply** its fields' counts (`(Bool, Bool) = 2 × 2 = 4`) and a **sum** type's values **add** its variants' counts (`OptionQ47. Does Go have sum types? Pin down the honest answer.
Answer
No — Go has no sum types as a language feature, and the proposal to add them has been repeatedly declined. You *approximate* them three ways: the `(value, error)` idiom (a built-in success-or-failure sum), a **sealed interface** (unexported marker method so only your package implements it) consumed by a type-switch, or a tagged struct. The crucial thing Go *can't* give you is **compile-time exhaustiveness**: nothing forces a type-switch to handle every implementer, so you lean on a `default: panic` and a linter (`go-exhaustive`). Saying "Go has sum types via interfaces" overstates it — it has a workaround that lacks the key guarantee.Q48. How many values does Either<Void, Bool> have, where Void is the empty type?
Answer
`2`. The empty type has `0` inhabitants, so `EitherQ49. ADTs vs OO class hierarchies — which "wins," and what does the Expression Problem say about the question?
Answer
Neither wins outright; the Expression Problem proves they're **dual**, optimizing opposite axes. ADTs make *new operations* cheap and *new variants* expensive (every match must change); class hierarchies make *new variants* cheap and *new operations* expensive (every class must gain a method). So the right question isn't "which is better" but "which axis will *this* code grow along?" Closed set of kinds + many operations (ASTs, protocol messages, state machines) → ADT. Open/extensible set of kinds + fixed operations (plugins, drivers, widgets) → hierarchy. A senior answer names the duality rather than picking a tribe.Q50. Adding an enum variant broke fifty files. Isn't that terrible ergonomics?
Answer
It's the opposite — it's the safety you're paying the type system for. The fifty compile errors are an *exact, exhaustive list* of every place that must consider the new case; the language did your impact analysis for free. The genuinely terrible scenario is the one without exhaustiveness: zero compile errors and fifty places that now silently mishandle the new variant, surfacing as scattered production bugs you find one outage at a time. If the fifty changes are mostly mechanical, that suggests the variant should perhaps carry behavior (or the matches should delegate to a method) — but the *breakage itself* is a feature, not a bug.Q51. If Option<T> is "just" null with a wrapper, why bother — isn't it the same at runtime?
Answer
At runtime the *representation* can be identical — `Option<&T>` is literally a possibly-null pointer thanks to niche optimization, so there's often **zero** space or speed cost. The difference is entirely at **compile time**: `OptionQ52. Can you model an infinitely-large set of values with a finite ADT definition?
Answer
Yes — via *recursion* or *unbounded* payload types. `ListRapid-Fire / One-Liners¶
Crisp answers; what an interviewer wants in one or two sentences.
Q53. Product vs sum, in one line each?
Answer
Product = "this **and** that" (struct/record, fields multiply); sum = "this **or** that" (enum/union, variants add).Q54. Option vs Result in one line?
Answer
`Option` = present or absent (no reason); `Result` = succeeded or failed-with-a-reason.Q55. Values in Bool × Bool? In Maybe Bool?
Answer
`Bool × Bool` = `2 × 2 = 4`; `Maybe Bool` (`OptionQ56. One sentence: "make illegal states unrepresentable"?
Answer
Shape the type so its cardinality equals the domain's — then forbidden combinations simply can't be constructed.Q57. "Parse, don't validate" in one sentence?
Answer
Convert input once into a narrower type that *carries the proof of validity*, instead of returning a boolean and passing the wide type onward to be re-checked.Q58. The Expression Problem in one sentence?
Answer
ADTs make adding *operations* easy and *variants* hard; class hierarchies make the reverse — you can't have both cheaply without extra machinery.Q59. Why is Option<&T> zero-cost?
Answer
A reference can't be null, so the compiler reuses the all-zeros pattern as the `None` tag (niche optimization) — same 8 bytes, no extra discriminant.Q60. Does Go have sum types?
Answer
No; you fake them with `(value, error)`, sealed interfaces, or tagged structs — and you lose compile-time exhaustiveness.Q61. Why is a wildcard _ arm risky on a domain enum?
Answer
It suppresses exhaustiveness, so a future variant compiles fine and is silently mishandled instead of flagged.Q62. What does a function type count as in the algebra?
Answer
Exponentiation: `A -> B` has `|B|^|A|` values.How to Talk About ADTs in Interviews¶
A few habits separate a strong answer from a textbook recital:
- Lead with the guarantee, not the syntax. Don't just say "I'd use an enum." Say what becomes impossible — "the connected-but-no-session state can't be constructed, so no consumer has to defend against it." The value of an ADT is the bug class it deletes.
- Reach for the algebra when reasoning about correctness. Counting inhabitants ("3 booleans = 8 states for a 3-state domain → 5 illegal") is a concrete, senior way to justify a refactor and to explain "make illegal states unrepresentable."
- Name the trade-off. Sums give exhaustiveness inside a binary but are not forward-compatible across versioned wire formats; "parse, don't validate" needs a private constructor to hold; niche optimization is free for
&T/NonZerobut not foru64. Acknowledging the other side is the senior signal. - Frame the OO comparison as the Expression Problem. "Which grows — kinds or operations?" beats a tribal "FP vs OO." It shows you understand the duality, not a slogan.
- Treat the "adding a variant broke everything" as a feature. The compiler enumerating every affected site is the safety you bought; the scary version is the one that compiles silently.
- Go deep only when invited. Memory layout, niche optimization, jump-table compilation, and
B^Aexponentials are great for "go deeper" prompts but shouldn't bury the practical point. - Use a concrete domain. Modeling a payment method, an HTTP response lifecycle, or a discount as a sum lands far harder than reciting "product and sum types."
Summary¶
- An ADT is built from products (fields combined — "and") and sums (alternatives — "or"), consumed by pattern matching; "algebraic" is literal — value counts multiply for products and add for sums, with
0(empty type) and1(unit type) andB^A(functions) completing a semiring. - The junior bar is the vocabulary plus everyday
Option/Result; the middle bar is domain modeling, choosingOptionovernull, recursive ADTs, and the per-language idioms (Rustenum, Javasealed+record, Python dataclass-union+match, Go sealed interface — with no Go sum types and no Go exhaustiveness). - The senior bar is designing with the type system: make illegal states unrepresentable (shrink cardinality to the domain), parse don't validate (carry the proof in a narrower type), the Expression Problem (ADTs ease operations, hierarchies ease variants), and evolving/versioning ADTs (exhaustiveness helps in one binary, needs an escape hatch across wire boundaries).
- The professional bar is the mechanics: type-counting isomorphisms, tagged-union layout and niche optimization (
Option<&T>is zero-cost;Option<u64>is not), why recursive ADTs need indirection, and how amatchlowers to jump tables / decision trees. - The recurring senior insight: an ADT's worth is the illegal states it deletes at compile time — judge the design by what it makes unrepresentable, and let the algebra (count the inhabitants) guide the modeling.
Related Topics¶
junior.md— products, sums, and readingOption/Result.middle.md— domain modeling and the per-language idioms.senior.md— illegal states, parse-don't-validate, the Expression Problem.professional.md— type counting, memory layout, match compilation.- Functional Programming roadmap — the paradigm these types come from.
- Map / Filter / Reduce and Composition (sibling FP topics) — the combinators that consume ADTs like
Option/Result. - Monads — Plain English (sibling FP topic) — why
Option/Result/Promisesharemap/flatMapand are all one idea. - Functional vs OO in Practice (sibling FP topic) — the Expression Problem trade-off in daily use.
- Clean Code → Pure Functions — total functions over precisely-shaped inputs.
In this topic
- interview