Plain `andThen`/`compose` assume total, pure functions. How do you compose steps that can fail (return `Result`) or are nullable, and what are the pitfalls of naive composition with side effects?
answer
- Plain andThen has no failure notion: throws/nulls/errors flow through
- Result combinator short-circuits via fold/map on first failure
- Nullable chain: this(a)?.let(g) stops at first null
- Side effects -> partial-failure and idempotency hazards
- Keep effectful commits at the edges, wrap in transactions
basics
~10 sBasic compose blindly feeds output to input, so failures or nulls flow through unhandled. Use a fail-aware combinator that short-circuits, e.g. compose functions returning Result so the first error stops the chain.
solid answer
~50 sStandard `f andThen g` is `{ g(f(x)) }` — it has no notion of failure, so if a step throws, the whole pipeline throws, and if a step returns `null` or an error sentinel, the next step receives it unchecked. To compose fallible steps cleanly, lift the chain into an effect type. For `(A)->Result<B>` functions you write a Kleisli-style combinator that short-circuits on failure: each step runs only if the previous succeeded, using `Result.map`/`mapCatching`/`fold`. For nullable steps, `?.let` chains or a `(A)->B?` combinator that stops at the first null. Side effects add ordering hazards: composition guarantees left-to-right execution, but you must ensure each step is idempotent or that partial failure leaves a sane state. The pitfall is treating composition as pure when steps mutate or throw — wrap effects explicitly rather than relying on naive output-to-input wiring.
code
kotlin · 8 linesinfix fun <A, B, C> ((A) -> Result<B>).andThenR(g: (B) -> Result<C>): (A) -> Result<C> =
{ a -> this(a).fold(onSuccess = g, onFailure = { Result.failure(it) }) }
val parse: (String) -> Result<Int> = { runCatching { it.toInt() } }
val recip: (Int) -> Result<Double> = { if (it == 0) Result.failure(ArithmeticException()) else Result.success(1.0 / it) }
val pipe = parse andThenR recip
println(pipe("0").isFailure) // true — short-circuits, recip's failure returned
println(pipe("x").isFailure) // true — parse fails, recip never runsgo deeper
Likely assumes composition just works and may not see failure/null hazards.
Recognizes exceptions propagate and can use runCatching, but may not build a short-circuiting Result combinator.
Designs Kleisli-style Result/nullable combinators, reasons about partial failure and idempotency of effectful steps.
Sets architecture: where effects live, transaction boundaries, and whether to adopt an effect type vs plain composition across the codebase.
## The assumption baked into plain composition `f andThen g == { x -> g(f(x)) }` wires output straight into input. That's perfect for **total, pure** functions. It breaks down when steps can: - **throw** (the exception escapes the whole pipeline), - return **`null`** (the next step gets null, possibly NPE), or - return an **error value** (passed along unchecked). ## Composing fallible steps with `Result` Kotlin's `Result<T>` models success/failure. To compose `(A) -> Result<B>` and `(B) -> Result<C>` so the first failure **short-circuits**, write a Kleisli-style combinator: ```kotlin infix fun <A, B, C> ((A) -> Result<B>).andThenR( g: (B) -> Result<C>, ): (A) -> Result<C> = { a -> this(a).fold(onSuccess = g, onFailure = { Result.failure(it) }) } ``` Now `parse andThenR validate andThenR save` stops at the first failed step and returns that failure — later steps never run. Inside a single fallible function you can also use `runCatching { ... }`, `Result.map`, and `mapCatching` to keep the effect contained. ## Composing nullable steps For `(A) -> B?` chains, short-circuit on the first null: ```kotlin infix fun <A, B, C> ((A) -> B?).andThenN(g: (B) -> C?): (A) -> C? = { a -> this(a)?.let(g) } ``` The `?.let` safe-call stops the chain when a step yields null, mirroring Result's short-circuit. ## Side-effect pitfalls Even with execution order guaranteed left-to-right, composing **effectful** steps is risky: - **Partial failure**: if step 3 of 5 throws, steps 1–2 already ran their effects (e.g. a row inserted). Composition gives you no rollback — you must add transactions or compensation. - **Idempotency**: retrying a composed pipeline re-runs all effects; ensure steps tolerate that or split pure transform from effectful commit. - **Hidden ordering**: readers assume andThen is pure transformation; effects buried in a stage surprise them. Name effectful stages clearly. ## Rule of thumb - Pure transforms → plain `andThen`/`compose`. - Fallible transforms → lift into `Result`/nullable combinators that short-circuit. - Effectful commits → keep them at the pipeline edges, wrap in a transaction, and don't pretend composition makes them safe. This keeps the declarative readability of composition while making failure and effects explicit instead of accidental.
- What happens if a middle step throws inside a plain `f andThen g` pipeline?The exception propagates out of the entire pipeline; no later step runs and there's no built-in recovery. You must wrap with runCatching/Result or try-catch.
- How does the Result combinator achieve short-circuiting?It folds each step's Result: on success it runs the next step, on failure it returns that failure immediately, so subsequent steps are skipped.
Plain compose is a pipe with no shutoff valve; a Result combinator adds a valve that closes the moment a stage leaks.
saying these in an interview costs you the question
- Assumes plain andThen handles failures or nulls automatically
- Composes effectful steps with no thought to partial-failure rollback
- Thinks a thrown exception in one stage is caught by composition
- Doesn't short-circuit — runs later steps after a failure
- Buries side effects mid-pipeline where readers expect pure transforms