Functions & Lambdas
In Kotlin a function is a first-class value, and almost everything in the topic follows from what that value compiles to. A function type (Int) -> String is not an abstraction: on the JVM it is the interface Function1<Integer, String>, so every non-inlined lambda is an object allocated on the heap, and its primitive arguments are boxed (Integer instead of int) in the process. That is exactly the cost inline removes (see Inline Functions). A lambda that captures a variable compiles to a class holding that captured state; unlike Java, where a captured variable must be effectively final, a Kotlin lambda can even capture a var — the compiler boxes it in a Ref object. And you can pass a higher-order function not only a lambda but a function reference (::isEven, String::length); a Java single-method interface takes a lambda through SAM conversion (for interfaces defined in Kotlin, fun interface fills that role).
The dividing line of the topic is what each construct actually is under the hood. An extension function and a lambda with a receiver are the same trick: the receiver is passed as a hidden first parameter. Hence the single most important fact in the topic: extensions are dispatched statically — they compile to a static method and are resolved by the declared type of the expression, not by the object's runtime type, so a class member always beats an extension and an extension overrides nothing. Operators are resolved not by their symbol but by a conventional name (plus, get, invoke, contains), and the function must be marked operator. And tailrec rewrites a tail self-call into a loop — but if the tail-position condition is not met, the compiler emits only a warning, and you are silently left with ordinary recursion. The layer-by-layer breakdown is below.
Topic map
- Higher-order functions — what "a function takes or returns a function" means, how the function type is built on the JVM, and why every lambda is an object.
- Trailing-lambda syntax — why a lambda-as-last-parameter moves outside the parentheses, and how that makes
apply {}and DSL calls read like language blocks. - Extension functions — the extension as a static method with the receiver as its first parameter: why it is not a member, cannot see
private, and is never overridden. - Extension properties — why they have no backing field, why a custom
get()is mandatory, and why an initializer is a compile error. - Lambda with a receiver — the type
T.() -> Unit,thisinstead ofit, the engine of builder DSLs, and@DslMarkeragainst outer-receiver leakage. - Local functions — a named function inside another that closes over the enclosing parameters and locals, and how it differs from a lambda held as a value.
- Operator overloading — operators as conventional names rather than symbols,
operatoras the mandatory keyword, and an operator extension on a class you do not own. - tailrec — rewriting a tail self-call into a loop, the tail-position conditions, and the silent warning instead of an error.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
| Confusing a top-level function with a higher-order one | Top-level is about where it is declared; higher-order is about a function taken as an argument or returned |
Thinking a function-type parameter must be an interface like Runnable | The type (Int) -> Boolean is self-sufficient; a lambda or the reference ::isEven is already a value of it |
| Forgetting a lambda's cost | A non-inlined lambda is a heap object and its primitive arguments are boxed; only inline cures it |
| Moving a lambda out of the parentheses when it is not the last parameter | Trailing-lambda syntax works only for the last function-type parameter |
Believing trailing-lambda syntax requires inline | The syntax is independent of inline — it is a pure call-parsing rule |
| Thinking an extension adds a member to the class | An extension is a static method with the receiver as its first parameter; nothing is added to the class |
| Expecting an extension to be virtual and override the base version | An extension is picked by the declared type of the expression; a member always beats an extension |
Marking a class open and expecting extension resolution to change | open has no effect on extension resolution — it is static by definition |
Writing val String.x = 1 and expecting the value to live on the string | An extension property has no backing field; an initializer is a compile error, a get() is required |
Confusing T.() -> Unit with (T) -> Unit | In the first the receiver is unqualified this; in the second the object arrives as the parameter it |
| Assuming the innermost receiver always wins in a nested DSL block | An outer receiver is in scope too; an unqualified call can go to it — which is what @DslMarker guards |
| Thinking a local function is just a lambda with a name | A local function closes over the enclosing locals directly and needs no function object per call |
Omitting operator and expecting + to compile | Without operator the operator form will not build; the name is fixed by convention too, not by the symbol |
| Assuming an operator must be a member of the class | An operator can be an extension — that is how + or [] is added to a class you do not own |
| Calling a recursive call a tail call while its result is still multiplied or added to | n * fact(n - 1) is not tail position; work remains on the result, so the frame cannot be dropped |
Expecting a compile error when tailrec does not apply | The compiler emits only a warning — you are silently left with plain recursion and StackOverflowError |
Interview relevance
This topic probes your execution model, not your vocabulary: the interviewer wants to hear what the construct compiles to. "How does an extension differ from a method?" — the right answer is not "the extension sits outside the class" but "an extension is a static method with the receiver as its first parameter, resolved by the declared type, so a member always overrides it and it can override nothing." The same move runs through the topic: what physically happens to a lambda when it captures a variable, why + will not compile without operator, at which precise moment tailrec becomes a loop and when it silently does not.
Typical checks:
- What a higher-order function is, how the function type is built on the JVM, and why a lambda is an object.
- When a lambda moves outside the parentheses and why trailing-lambda syntax does not require
inline. - That an extension is dispatched statically: a member wins, a subclass gets the base version,
privateis off limits. - Why an extension property has no backing field and why an initializer is forbidden.
- How a lambda with a receiver
T.() -> Unitmakes a builder DSL possible, and what@DslMarkerfixes. - How a local function differs from a lambda held as a value, and what it closes over.
- That operators are resolved by name rather than by symbol, and that an operator can be an extension.
- How
tailrecremoves the stack cost, and why breaking its condition is a warning rather than an error.
Common wrong answer: "an extension function is a way to add a method to a class, and it is virtual, so a subclass gets its own version." In reality nothing is added to the class: an extension compiles to a static method taking the receiver as its first parameter, and it is chosen by the declared type of the expression. A List<Notification> is Notification for each element, so even on an Urgent the Notification version runs, not a "more specific" one. An extension takes no part in virtual dispatch and overrides nothing; if a member with the same signature exists nearby, the member wins. Want polymorphism — make the behaviour an open member and override it, and keep extensions for classes you cannot modify.