skip to content

How do you build a readable multi-step pipeline by composing function values, and how do method references (`::`) and bound references fit in?

level: middleimportance: should knowfreq 38%

answer

  1. Pipeline = single composed function via andThen chain
  2. :: makes function values: unbound (receiver first), bound (captures instance), constructor
  3. Reads in execution order vs inside-out nesting
  4. Lambdas and references both compose freely
  5. Compiler enforces output->input wiring per link

basics

~10 s

Chain steps with andThen to form one pipeline function. You can plug in lambdas or method references like String::trim or obj::save so each step reads as a named operation.

solid answer

~40 s

A pipeline is a single composed function: `val process = parse andThen normalize andThen validate andThen persist`. Each operand is a function value of type `(A)->B`. Method references (`::`) turn existing functions into values: `String::trim` (unbound, takes the receiver as first arg), `repo::save` (bound, captures `repo`), or `::topLevelFn`. They slot directly into composition because their type is `(In)->Out`. Composition stays lazy, so `process` is just a recipe until invoked. Compared to nesting `persist(validate(normalize(parse(x))))`, the `andThen` chain reads top-to-bottom in execution order and names each stage. Watch the wiring: each step's output type must match the next step's input type, which the generic helper enforces at compile time.

code

kotlin · 5 lines
kotlin
infix fun <A, B, C> ((A) -> B).andThen(g: (B) -> C): (A) -> C = { g(this(it)) }

fun parse(s: String): List<String> = s.split(",")
val pipeline = ::parse andThen List<String>::size andThen { "count=$it" }
println(pipeline("a,b,c")) // count=3

go deeper

for a junior

Can chain two steps with andThen and use a simple lambda or ::name reference.

for a middle

Builds multi-step pipelines, knows unbound vs bound references and their types, mixes lambdas and references.

for a senior

Argues readability and type-safety benefits, and knows when sequence operators are the better composition tool.

for a principal

Sets guidance on where named-pipeline composition pays off vs over-engineering, considering testability and reuse.

## Building the pipeline Composition shines when you have several single-purpose functions and want one combined operation. Using the `andThen` helper: ```kotlin val process: (String) -> Report = ::parse andThen ::normalize andThen ::validate andThen ::render val report = process(rawInput) ``` This reads in **execution order** (left to right), unlike nested calls `render(validate(normalize(parse(raw))))`, which read inside-out. ## Method references (`::`) as function values The `::` operator produces a **callable reference** — a function value you can compose: - **Top-level / local**: `::parse` → type `(String) -> Parsed`. - **Unbound class member**: `String::trim` → type `(String) -> String`; the receiver becomes the **first parameter**. So `String::length` is `(String) -> Int`. - **Bound reference**: `repo::save` captures a specific `repo` instance → type `(Entity) -> Unit`. The instance is fixed when the reference is created. - **Constructor reference**: `::User` → a function returning a `User`. Because each of these *is* a `(In) -> Out` value, they drop straight into `andThen`/`compose` with no wrapper lambda. ```kotlin val trimmedLength = String::trim andThen String::length // (String) -> Int ``` ## Mixing lambdas and references ```kotlin val clean: (String) -> String = { it.lowercase() } val pipeline = clean andThen String::trim andThen { it.take(10) } ``` Lambdas and references interoperate freely since both are function values. ## Type wiring Every `andThen` link demands output-of-step-N == input-of-step-N+1. The generic helper threads these types, so a mismatched stage is a **compile error**, not a runtime surprise. This is the main reliability benefit over manual nesting where a wrong order may still type-check by accident. ## When to prefer this - Reusable, named, branch-free pipelines (parse → validate → transform). - When you want to pass the whole pipeline around as one value. ## When NOT to - If you have a collection, prefer `Sequence`/`Iterable` operators (`map`, `filter`) which already compose lazily. - For one-off two-step logic, a direct call is clearer than building helpers.

  • What is the type of an unbound reference like `String::length`?
    `(String) -> Int`. The receiver becomes the first parameter, so it composes like any (String)->Int function.
  • When should you reach for collection operators instead of building an andThen chain?
    When the data is a collection/sequence; map/filter/etc. already compose lazily and read better than wrapping element transforms in andThen.

Method references are like pre-labeled hoses; andThen just clamps them end to end into one pipe.

saying these in an interview costs you the question

  • Thinks an unbound member reference's receiver is hidden rather than the first parameter
  • Cannot distinguish bound vs unbound references
  • Builds pipelines for collection element work where map/filter would be clearer
  • Claims method references can't be composed because they're 'not lambdas'
  • Ignores that each link's types must align

context