Implement a reusable generic Result type as a sealed hierarchy with Success<T> and Error cases. How do generics interact with object subtypes, and how would you write a `map` that transforms the success value?
answer
- out T = covariant, allows Result<Nothing> failure
- Error : Result<Nothing>, Nothing is bottom type
- object can't have its own type parameter
- map: smart-cast, transform Success, pass Error
- Sealed -> when needs no else
basics
~10 sMake a sealed type Result<T> with Success holding a T and Error holding a throwable. Add a map function that, if it's Success, transforms the value, and if it's Error, passes it through unchanged.
solid answer
~40 sDeclare `sealed interface Result<out T>` with `data class Success<T>(val value: T)` and `data class Error(val cause: Throwable)`. Marking the type parameter `out` (covariant) lets `Result<Dog>` be used where `Result<Animal>` is expected and lets `Error` (which holds no `T`) be a single subtype rather than `Error<T>`. A stateless `object` subtype generally cannot reference the class's type parameter, so either keep `Error` a `data class` carrying the cause, or with `out` you can model a no-value case. `map` smart-casts on the case: transform on `Success`, return the same `Error` on failure. Prefer this over exceptions for expected failures because the caller is forced by exhaustiveness to handle both outcomes.
code
kotlin · 13 linessealed interface Result<out T> {
data class Success<out T>(val value: T) : Result<T>
data class Error(val cause: Throwable) : Result<Nothing>
}
inline fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> = when (this) {
is Result.Success -> Result.Success(transform(value))
is Result.Error -> this
}
// usage
val r: Result<Int> = Result.Success(2)
val doubled: Result<Int> = r.map { it * 2 } // Success(4)go deeper
Writes the two-case sealed Result and a basic when; may not understand variance.
Uses out T and Result<Nothing>, writes map with smart casts, explains why no else is needed.
Explains the Nothing bottom-type mechanism, inline for zero-alloc helpers, and when to prefer kotlin.Result vs a custom domain hierarchy.
Designs a project-wide error ADT (typed error subtypes), weighs sealed Result vs exceptions vs Arrow Either, and sets library boundaries.
## Goal A reusable two-case ADT: an operation either **succeeds** with a value of type `T` or **fails**. This is the `Result`/`Either` shape. ## Generics + variance ```kotlin sealed interface Result<out T> { data class Success<out T>(val value: T) : Result<T> data class Error(val cause: Throwable) : Result<Nothing> } ``` - `out T` makes `Result` **covariant**: if `Dog : Animal`, then `Result<Dog>` is a subtype of `Result<Animal>`. Covariance is allowed because `T` only appears in *out* positions (return/read), never as a function parameter. - `Error : Result<Nothing>` is the key trick. `Nothing` is Kotlin's **bottom type** — a subtype of every type. Because of covariance, `Result<Nothing>` is assignable to any `Result<T>`. So a single `Error` value works as a failure for *every* `T` without making `Error` generic. - An `object` (singleton) **cannot** declare its own type parameter, which is why a value-less case usually extends `Result<Nothing>`. Here `Error` carries a `Throwable`, so it stays a `data class`, but it still extends `Result<Nothing>` because it holds no `T`. ## `map` over the ADT ```kotlin inline fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> = when (this) { is Result.Success -> Result.Success(transform(value)) is Result.Error -> this // smart-cast to Error: Result<Nothing>, valid as Result<R> } inline fun <T> Result<T>.getOrElse(fallback: (Throwable) -> T): T = when (this) { is Result.Success -> value is Result.Error -> fallback(cause) } ``` - The `when` is **exhaustive** because the type is sealed — no `else` branch needed, and adding a third case would force a compile error here. - After `is Result.Success`, `this` is smart-cast so `value` is directly accessible (type `T`). - `inline` lets the lambda be non-capturing and avoids an allocation per call; it is optional but idiomatic for tiny higher-order helpers. - In the `Error` branch, returning `this` type-checks because `Error : Result<Nothing>` and `Result<Nothing> <: Result<R>`. ## Why prefer this to throwing Exceptions are invisible in the type signature; `Result<T>` makes failure **part of the contract**, and exhaustiveness forces callers to deal with it. Kotlin's stdlib also ships `kotlin.Result<T>` (an inline value class) for the common case — but a hand-rolled sealed `Result` lets you add domain-specific error subtypes (e.g. `NotFound`, `Unauthorized`).
- Why can Error extend Result<Nothing> and still be used as Result<String>?Because Result is covariant (out T) and Nothing is a subtype of every type, so Result<Nothing> is a subtype of Result<String>. The single Error instance therefore satisfies any Result<T>.
- Why is `this` valid in the Error branch of map even though map returns Result<R>?After the is-check it is smart-cast to Error, whose type is Result<Nothing>; covariance makes Result<Nothing> assignable to Result<R>.
Like a sealed envelope marked either 'cheque inside' or 'rejection letter' — map only re-prints the cheque amount; a rejection letter is forwarded untouched.
saying these in an interview costs you the question
- Making Error generic (Error<T>) and threading T through failures needlessly
- Not knowing Nothing is the bottom type
- Forgetting that out T is required for the Result<Nothing> trick
- Adding an else branch to a sealed when 'just in case'
- Claiming object subtypes can hold a type parameter