skip to content

Walk through what the compiler actually generates when an `inline` HOF is called. Address: the lambda object, the function-body copy, captured variables, and one thing inlining does NOT do.

level: seniorimportance: should knowfreq 45%

answer

  1. Macro-like expansion at compile time
  2. Lambda object eliminated, body spliced in
  3. Captured var mutable, no Ref wrapper
  4. Removes wrapper+dispatch, not the work
  5. Lambda can't escape -> noinline/crossinline

basics

~20 s

At each call, the compiler drops the inline function's body and your lambda's body straight into the caller. No object is made for the lambda, and variables it uses are just read in place. It does not, however, run anything concurrently or change results.

solid answer

~50 s

For a call like `foo(arg) { lambdaBody }` where `foo` is `inline`, the compiler does NOT emit a `FunctionN` instance for the lambda. Instead it **copies `foo`'s compiled body into the caller**, and wherever `foo` invokes its lambda parameter, it **splices in `lambdaBody`** directly. Variables the lambda **captures** from the enclosing scope are referenced **in place** — no closure object is allocated to hold them, and (unlike non-inline lambdas) even `var`s can be mutated without the `Ref` wrapper trick. The lambda parameter exists only at compile time; you can't store it in a field or pass it on (that's where `noinline`/`crossinline` come in). What inlining does NOT do: it doesn't change semantics or evaluation order, doesn't move work to another thread, and doesn't eliminate the work itself — it eliminates the *wrapper object and dispatch*, not the lambda's actual statements. The cost it adds is duplicated bytecode at every call site.

code

kotlin · 10 lines
kotlin
inline fun retry(times: Int, action: () -> Unit) {
    for (i in 0 until times) action()
}

fun demo(name: String) {
    var count = 0
    retry(3) { count++; println(name) }
    // Compiles ~ to an inlined for-loop mutating `count` directly,
    // with NO Function0 object created for the lambda.
}

go deeper

for a junior

Understands the body is copied and no lambda object is made.

for a middle

Can describe splicing the lambda body where the param is called and captures being read in place.

for a senior

Explains the mutable-captured-var consequence, the no-escape restriction (pointing to noinline/crossinline), and that work itself isn't removed.

for a principal

Articulates the macro-expansion mental model with semantic-preservation guarantees and quantifies the code-size trade-off across call sites.

## Step-by-step codegen Take a concrete inline HOF and a call site: ```kotlin inline fun retry(times: Int, action: () -> Unit) { for (i in 0 until times) action() } fun demo(name: String) { var count = 0 retry(3) { count++; println(name) } } ``` ### 1. The lambda object is NOT created Normally `{ count++; println(name) }` would compile to a `Function0` object. With `retry` inline, **no such object exists** in the bytecode. ### 2. The function body is copied in The compiler copies `retry`'s body — the `for (i in 0 until times) action()` loop — into `demo`. So `demo` ends up containing the loop directly. ### 3. The lambda body is spliced where the param is called Wherever `retry` wrote `action()`, the compiler substitutes the **lambda's statements**. Result is equivalent to: ```kotlin fun demo(name: String) { var count = 0 for (i in 0 until 3) { count++ println(name) } } ``` ### 4. Captured variables are referenced in place The lambda captures `count` and `name`. Because the body is inlined into `demo`, those locals are accessed **directly**. Two consequences: - **No closure object** stores them. - A captured **`var` like `count` can be reassigned** inside the inlined lambda. (A non-inline lambda can only *read* effectively-final captures or needs a `Ref.IntRef` wrapper; inlining sidesteps that entirely.) ## What inlining does NOT do - **It does not change semantics or order of evaluation.** Same results as the non-inline version. - **It does not run anything on another thread or asynchronously.** - **It does not remove the lambda's work** — `count++; println(name)` still executes `times` times. It removes the *object + `invoke()` dispatch*, not the logic. - **It does not let the lambda escape.** Because there's no object, you can't store the inlined lambda in a property, return it, or call it from a nested non-inline lambda — the compiler errors. Marking a param `noinline` (keep it a real object) or `crossinline` (inline but forbid non-local return so it can cross into another context) handles those needs; both are separate facets. ## The trade-off, precisely The duplicated body lives at **every call site**, so binary size grows with call-site count × body size. For a tiny HOF that's nothing; for a large one it's the dominant downside. ## Mental model Think of the inline HOF and its lambdas as a **macro the Kotlin compiler expands**, with the strong guarantee that semantics are preserved — you trade a little code size for zero allocation and zero dispatch.

  • Why can an inlined lambda mutate a captured `var` but a non-inline one effectively can't?
    An inlined lambda's body runs directly in the enclosing frame, so it accesses the local `var` like any other statement. A non-inline lambda becomes a separate object that can't reassign the original local, so the compiler boxes it in a Ref wrapper to allow mutation.
  • Why does the compiler reject storing an inline lambda parameter in a field?
    Because there is no object to store — the lambda exists only as inlined code. To keep it as a real callable object you must mark the parameter `noinline`.

saying these in an interview costs you the question

  • Saying inlining removes the lambda's actual work, not just the wrapper
  • Claiming captured vars still need a Ref wrapper under inlining
  • Thinking you can return/store the inlined lambda freely
  • Asserting inlining changes evaluation order or results
  • Confusing inline expansion with JIT method inlining at runtime

context