Why are stdlib helpers like the scope functions (`let`, `run`, `apply`, `also`, `with`) and `repeat` declared `inline`, and what does that buy you?
answer
- inline = body + lambda pasted at call site
- no Function object, no invoke() call
- enables non-local return from lambda
- crossinline forbids it; noinline keeps a real object
- reified needs inline; bytecode bloat is the cost
basics
~20 sThese helpers are marked inline, so the compiler copies their code straight into the call site. That avoids creating an extra function object for the lambda, making them as cheap as writing the code by hand.
solid answer
~50 sThe scope functions and helpers like `repeat`, `use`, `takeIf`, and `runCatching` are `inline` functions taking lambda parameters. At compile time the function body and the lambda are **inlined** into the call site, so no `Function`/`Lambda` object is allocated and there is no virtual call to `invoke()` — the abstraction 'compiles away' to roughly what you'd write by hand. Inlining also enables **non-local returns**: a bare `return` inside the lambda returns from the enclosing function. Parameters can be tuned: `crossinline` forbids non-local returns (needed when the lambda is used in another context), and `noinline` keeps a specific lambda as a real object (e.g. to store or pass it on). `reified` type parameters are only possible on inline functions, which is why `filterIsInstance<T>()` works. The trade-off: inlining duplicates bytecode at every call site, so very large inline functions bloat code — that is why only small lambda-taking helpers are inline.
code
kotlin · 2 linesinline fun <reified T> Any?.asTypeOrNull(): T? = this as? T
// reified lets `as? T` work at runtime; only possible because the fn is inlinego deeper
Knows inline avoids creating an extra object for the lambda.
Explains zero-cost abstraction and that forEach/let are inline so there's no allocation.
Covers non-local returns, crossinline/noinline, reified, and the bytecode-bloat trade-off.
Frames inline higher-order functions as the stdlib's strategy for library-as-syntax, and weighs code-size vs allocation across hot paths and public-API binary compatibility.
## What `inline` means Marking a function `inline` tells the compiler to **substitute the function body — and the bodies of its lambda arguments — directly at each call site** instead of making a real call. For higher-order functions this is the key optimization, because normally each lambda becomes an object implementing a `FunctionN` interface, and calling it is a virtual `invoke()`. ## Why the stdlib uses it for scope functions `let`, `run`, `with`, `apply`, `also`, plus `repeat`, `use`, `takeIf`, `takeUnless`, and `runCatching` all take lambdas and are tiny. Making them `inline` means: - **No allocation**: the lambda isn't turned into an object — zero GC pressure for `list.forEach { ... }`-style code. - **No call overhead**: the body is pasted in; there's no `invoke()` dispatch. - **Zero-cost abstraction**: `x?.let { it.foo() }` is as efficient as a hand-written null check + call. ```kotlin val r = repeat(3) { println(it) } // body inlined, no Function object created ``` ## Non-local returns Because an inline lambda's body is part of the caller, a plain `return` inside it returns from the **enclosing** function: ```kotlin fun find(list: List<Int>): Int? { list.forEach { if (it > 0) return it } // returns from find, not just the lambda return null } ``` This only works because `forEach` is inline. ## Modifiers on inline lambda parameters - **`noinline`**: keep this particular lambda as a real object — needed when you must store it, return it, or pass it to a non-inline function. - **`crossinline`**: the lambda is still inlined but **non-local returns are forbidden**, required when the lambda is invoked from a different execution context (e.g. inside another lambda/object). - **`reified`**: an inline function may mark a type parameter `reified`, so `T` is known at runtime; this powers `filterIsInstance<T>()`, `instanceOf`-style checks, and `inline fun <reified T> Gson.fromJson(...)`. ## Trade-offs - Inlining **duplicates bytecode** at every call site → larger code size if the function is big or called in many places. Hence inline is for **small** helpers. - The compiler warns when `inline` provides no benefit (e.g. a function with no lambda parameters and no reified type) — it's mostly useful for lambda-taking or reified functions. ## Why it fits the 'stdlib model' theme This is the unifying idea behind much of the stdlib: rather than baking control constructs into the language, Kotlin ships them as **inline higher-order functions** that compile away, so library code reads like syntax but costs like handwritten code (`use` ≈ try-with-resources, `repeat` ≈ a for loop, scope functions ≈ explicit blocks).
- Why can only inline functions have reified type parameters?Inlining pastes the actual type argument at the call site, so the type is known concretely at runtime instead of being erased.
- When would you mark a lambda parameter crossinline?When the lambda is invoked from a nested context (another lambda/object), so non-local returns must be disallowed.
saying these in an interview costs you the question
- Saying inline makes runtime faster by JIT only, missing allocation/call elimination
- Claiming every function should be inline
- Not knowing inline enables non-local returns
- Confusing crossinline and noinline
- Thinking reified works without inline