Explain how `inline` lets stdlib scope functions like `let`, `run`, and `with` be used freely without allocation overhead. What would change if they were NOT inline?
answer
- Scope functions are inline one-liners
- name.let { } compiles like inline body
- Non-inline = Function1 alloc + capture + invoke()
- Hot loops multiply the saved cost
- Inlining also enables non-local return
basics
~20 sThese helpers are tiny functions that take a lambda. Because they are marked inline, the compiler pastes their body and your lambda into the call site, so no extra object is made. If they were not inline, every call would create a lambda object.
solid answer
~50 s`let`, `run`, `with`, `apply`, and `also` each just take a lambda and call it on a receiver/argument. They are declared `inline`, so at every call site the compiler substitutes both the scope function's body and your lambda body directly into the caller — no `Function1` object is allocated and no virtual `invoke()` happens. That makes them effectively free compared with writing the equivalent code by hand, even inside hot loops. If they were not inline, each invocation would (a) allocate a `FunctionN` instance capturing any referenced variables, and (b) dispatch through `invoke()`. In a tight loop calling `list.forEach { ... }` thousands of times, that becomes real GC and dispatch overhead. Inlining also gives a side benefit: because the lambda body is inlined, a `return` inside it is a non-local return from the enclosing function — only possible because the lambda isn't a separate object.
code
kotlin · 7 lines// Declared inline in the stdlib:
public inline fun <T, R> T.run(block: T.() -> R): R = block()
// Call site:
val area = rect.run { width * height }
// Inlined ~ to:
val area = rect.width * rect.height // no Function1 allocatedgo deeper
Knows the scope functions take a lambda and are cheap to use.
Explains that inline removes the Function1 allocation and invoke() dispatch, and why that matters in loops.
Adds the closure/capture nuance and that inlining also unlocks non-local return and reified.
Weighs readability-vs-codegen-size at scale and reasons about when the JIT could or couldn't recover the same wins for non-inline closures.
## The scope functions are just inline HOFs The stdlib **scope functions** are thin wrappers around a lambda. Simplified, `let` is: ```kotlin public inline fun <T, R> T.let(block: (T) -> R): R = block(this) ``` The `inline` modifier is the whole point. Without it, `let` would be an ordinary function taking a `Function1<T, R>` parameter. ## What inlining does at the call site Consider: ```kotlin val upper = name.let { it.trim().uppercase() } ``` Because `let` is `inline`, the compiler replaces this with code equivalent to: ```kotlin val upper = name.trim().uppercase() ``` - **No `Function1` object** is created to hold `{ it.trim().uppercase() }`. - **No `invoke()`** indirection. - The receiver `this`/`it` binding is resolved at compile time. ## What it would cost if they were NOT inline If `let` were a plain function, each call would: 1. **Allocate a lambda object** implementing `Function1`. If the lambda **captures** variables from the enclosing scope (a **closure**), the object also stores those captured references — more allocation. 2. **Dispatch virtually** through `invoke()` each time the lambda runs. In isolation that's cheap, but multiply by a hot loop: ```kotlin for (line in millionsOfLines) { line.let { parse(it) } // inline -> 0 extra objects } ``` Non-inline, that's millions of `Function1` allocations and dispatches — measurable GC pressure and slowdown. ## Bonus capabilities inlining enables Because the lambda is inlined (not a separate object), these become possible **only** for inline HOFs: - **Non-local `return`**: `return` inside the lambda returns from the enclosing function. - **`reified` type parameters**: the type is known at the call site, so it can be materialized. (These belong to other facets of inlining but illustrate why `inline` is more than a micro-optimization.) ## Practical takeaway Reach for `let`/`run`/`with`/`apply`/`also`/`takeIf`/`forEach` freely — they're inline, so the readability never costs allocation. The only real cost of inlining is **bytecode duplication** at each call site, which is negligible for these tiny bodies.
- Does a captured variable change the allocation story?For an inline lambda, no separate closure object is allocated regardless of captures — the captured locals are just referenced inline. For a non-inline lambda, captures must be stored on the allocated FunctionN object.
- Are `apply` and `also` also inline?Yes. All five scope functions (let/run/with/apply/also), plus takeIf/takeUnless and repeat, are inline in the stdlib.
saying these in an interview costs you the question
- Saying scope functions allocate a lambda each call
- Claiming inlining only helps because the functions are 'simple', not because of the modifier
- Confusing which scope functions return the receiver vs. the lambda result with the allocation question
- Asserting inlining changes thread/coroutine behavior
- Thinking JIT inlining makes the `inline` keyword redundant for closures (JIT can't always avoid the allocation)