Across a codebase, sealed state types accumulate scattered `when (state)` blocks. A teammate proposes adding a `fold`/visitor-style method on the sealed type instead. When is encapsulating behaviour on the ADT worth it, and what do you lose?
answer
- fold = one lambda per case, still when inside
- when wins: smart casts, locals, early return, guards
- Sealed when already gives exhaustiveness; fold adds DRY
- Visitor = ceremony Kotlin rarely needs
- Edge: when; reusable algebra: extension fold/map
basics
~20 sA fold is a single method that takes one function per case and returns a result, so callers don't write their own when each time. It's handy for common reductions, but plain when is clearer for one-off logic and keeps the data and behaviour separate.
solid answer
~50 sA sealed ADT is naturally 'data-centric': callers branch with exhaustive `when`. Adding a `fold(onLoading, onSuccess, onError): R` (or a `Visitor`) centralizes the branching so the case list lives in one place and call sites read declaratively. It pays off when the *same* reduction is repeated widely, or when you want to discourage `else`-laden ad-hoc branching. The cost: `fold` fixes one shape (a function per case) and loses the ergonomics of `when` — smart casts across guards, capturing multiple locals, early `return`, and Kotlin 2.x guard conditions (`is Success if it.items.isEmpty()`). Exhaustiveness is already guaranteed by sealed `when`, so `fold` adds little *safety*; its value is DRY and discoverability. For UI state, prefer `when` at the rendering edge and reserve `fold`/extension helpers (`map`, `getOrElse`) for genuinely reusable transformations. Avoid heavyweight Visitor interfaces — they fight Kotlin's pattern-matching grain.
code
kotlin · 10 linesinline fun <T, R> Result<T>.fold(
onSuccess: (T) -> R,
onError: (Throwable) -> R,
): R = when (this) {
is Result.Success -> onSuccess(v)
is Result.Error -> onError(e)
}
// reusable reduction at many call sites:
val msg = result.fold(onSuccess = { "ok: $it" }, onError = { "fail: ${it.message}" })go deeper
Can write a when over the sealed type but likely hasn't seen fold.
Understands fold takes a lambda per case and removes duplication; may not weigh the trade-offs.
Compares fold vs when on smart casts, guards, and DRY; knows fold is when underneath.
Sets a codebase convention (edges use when, reusable algebra uses fold), judges API-stability needs, and rejects unnecessary Visitor ceremony.
## Two styles over an ADT **Data-centric (idiomatic Kotlin):** the sealed type is dumb data; behaviour lives in `when` at call sites or in extension functions. ```kotlin val label = when (state) { Loading -> "…" is Success -> state.title is Error -> state.message } ``` **Behaviour-centric (fold / Church-encoding / Visitor):** the type exposes a method that takes one lambda per case. ```kotlin sealed interface Result<out T> { data class Success<T>(val v: T) : Result<T> data class Error(val e: Throwable) : Result<Nothing> } inline fun <T, R> Result<T>.fold( onSuccess: (T) -> R, onError: (Throwable) -> R, ): R = when (this) { is Result.Success -> onSuccess(v) is Result.Error -> onError(e) } ``` Note `fold` is itself implemented with `when` — it doesn't replace exhaustiveness, it *encapsulates* it. ## When `fold`/visitor is worth it - **Repeated reduction**: the same case-handling appears in many places; a single `fold` removes duplication and gives one edit point when a case is added. - **Library/API surface**: you expose the ADT but want callers to handle every case without seeing the constructors, or you want a stable method even if internals change. - **Composability**: `map`, `flatMap`, `getOrElse`, `fold` form a small algebra that chains cleanly — much nicer than nested `when`. ## What you lose vs `when` - **Smart casts & locals**: inside a `when` branch you can read several outer variables, early-`return`, `break`, and chain further `is` checks; a `fold` lambda is a closed expression returning `R`. - **Guard conditions (Kotlin 2.1+)**: `when` supports `is Success if value.isEmpty() -> …` guards; `fold` can't express sub-conditions without re-branching inside the lambda. - **Readability for one-offs**: for a single call site, `fold` with N positional lambdas is *less* readable than a labelled `when`. - **Adding a case**: with `when` as an *expression* the compiler flags every site; with one `fold` you change the signature once — good for DRY, but you also lose the compiler walking you through each unique handling site. ## Visitor specifically The classic GoF Visitor (an interface with `visitX` methods) exists to get exhaustiveness in languages *without* pattern matching. Kotlin already gives exhaustiveness via sealed `when`, so a full Visitor is usually ceremony. A `fold`/`when`-based extension is the lightweight, idiomatic equivalent. ## Principal-level guidance Default to **`when` at the edges, extension functions for reusable algebra**. Introduce `fold`/visitor only when duplication or API-stability concerns justify it, and document it as the sanctioned reduction so the codebase doesn't grow both styles randomly.
- Does adding fold improve type safety over exhaustive when?No. Sealed when already forces handling every case at compile time. fold's benefits are DRY, encapsulation, and composability — not extra safety.
- What Kotlin when feature is hard to replicate inside a fold lambda?Guard conditions (e.g. `is Success if value.isEmpty()`), introduced in Kotlin 2.1, plus early return and reading multiple outer locals across branches.
fold is a vending machine with one button per item — great when everyone wants the same snacks; a free-form kitchen (when) is better for cooking something bespoke once.
saying these in an interview costs you the question
- Claiming fold/visitor adds exhaustiveness that when lacks
- Pushing a full GoF Visitor as idiomatic Kotlin
- Replacing all when with fold, losing smart casts and guards
- Not realizing fold is implemented with when internally
- No stance on edge-when vs reusable-fold convention