As an API designer, why does the position of a function-type parameter matter, and how do you order parameters to give callers clean trailing-lambda call sites?
answer
- Lambda last → trailing call site
- Defaults on other params → drop ()
- Receiver lambda T.() -> Unit → DSL block
- @DslMarker scopes the receiver
- Reordering params later is a breaking change
basics
~10 sOnly the last parameter can be a trailing lambda, so you put the main callback last. That way callers write a clean block after the parentheses instead of a noisy nested lambda.
solid answer
~40 sBecause the trailing-lambda convention is restricted to the **final** parameter, the position of a function-type parameter is an API-design decision, not a cosmetic one. Put the primary lambda **last** so callers get `process(timeout) { ... }` instead of `process({ ... }, timeout)`. Give other parameters **defaults** so the parentheses can collapse to `process { ... }`. For builder/config APIs, take a single **function literal with receiver** (`T.() -> Unit`) as the last parameter so the call reads like a block with `this` bound to the builder — the foundation of Kotlin DSLs (`buildString`, `apply`, kotlinx.html). Avoid trailing the lambda when it would be surprising (e.g. an optional callback that's rarely supplied). This ordering also affects source compatibility: reordering parameters later is a breaking change for named-argument callers, so get the lambda-last shape right early.
code
kotlin · 8 lines// lambda-last + receiver lambda = idiomatic builder
fun html(block: HtmlBuilder.() -> Unit): String =
HtmlBuilder().apply(block).render()
val page = html {
head { title("Home") }
body { p("hello") }
}go deeper
Knows the lambda should go last but can't fully justify why.
Explains lambda-last gives a clean call site and uses defaults to drop the parentheses.
Designs receiver-lambda DSLs, applies @DslMarker, and reasons about ergonomics trade-offs.
Weighs API evolution/compatibility, DSL vs flat API, and overload ambiguity when shaping the signature.
## The design lever Only the **last** parameter participates in the trailing-lambda convention. So *which* parameter is last is a deliberate ergonomics choice. ### Put the primary lambda last ```kotlin // good: lambda last → clean call site fun withTransaction(timeoutMs: Long = 5000, block: () -> Unit) { /* ... */ } withTransaction(2000) { doWork() } withTransaction { doWork() } // timeout defaults; () dropped // awkward: lambda not last → no trailing form fun withTransaction2(block: () -> Unit, timeoutMs: Long = 5000) { /* ... */ } withTransaction2({ doWork() }, 2000) // can't trail; noisy ``` ### Defaults let parentheses vanish Giving every non-lambda parameter a default means callers can drop `()` entirely (`withTransaction { ... }`), matching the look of `run { }`. ## Receiver lambdas for DSLs A **function literal with receiver**, type `Builder.() -> Unit`, makes `this` inside the block the builder, so the block reads like a mini-language: ```kotlin fun buildReport(block: ReportBuilder.() -> Unit): Report { val b = ReportBuilder() b.block() // invoke with b as receiver return b.build() } buildReport { title("Q2") section { line("revenue up") } } ``` This is exactly how `buildString`, `apply`, `buildList`, and `kotlinx.html` work. Combine with `@DslMarker` to prevent accidentally calling outer-receiver methods from an inner block. ## Compatibility caveat Parameter order is part of the public contract for **named-argument** and positional callers. Moving the lambda to last *after* release breaks those callers, so decide the lambda-last shape up front. ## When NOT to trail If the function-type parameter is an optional, rarely-used callback, trailing it can mislead readers into thinking it's the main action. In that case keep it earlier with a default `{}` and let a different parameter or none be last.
- Why give non-lambda parameters default values?So that when callers omit them, the parentheses become empty and can be dropped, yielding the clean `fn { ... }` call site.
- What does @DslMarker solve in receiver-lambda DSLs?It restricts implicit access to outer receivers, so inside a nested block you can't accidentally call methods of an enclosing builder, preventing scope-leak bugs.
Like designing a tool's handle to be where the hand naturally lands — put the lambda where the caller's block naturally goes.
saying these in an interview costs you the question
- Puts the lambda parameter first 'for readability', killing the trailing form
- Unaware that defaults let callers drop the parentheses
- Doesn't know function-literal-with-receiver underpins DSLs
- Ignores that reordering parameters breaks named-argument callers