Functions & Lambdas
Kotlin as a functional language — higher-order functions, function types, lambdas with a receiver and the builder DSLs they enable, trailing-lambda syntax, local functions, tailrec, operator overloading, and extension functions and properties.
13 questions
JuniorTheoryVery commonWhat is an extension function in Kotlin, and how does its call site look?
What is an extension function in Kotlin, and how does its call site look?
An extension function is declared outside the class with that class as its receiver: fun String.shout() = uppercase(). Inside it this is the receiver, and the call site reads like a member, yet nothing is added to the class.
Common mistakes
- ✗Believing an extension really adds a member to the class
- ✗Expecting an extension to reach the receiver's
privatemembers - ✗Thinking a subclass can override an extension function
Follow-up questions
- →What happens when an extension has the same signature as an existing member function?
- →Why may an extension be declared on a nullable receiver such as
String??
JuniorTheoryVery commonWhat is a higher-order function in Kotlin, and how do you pass a lambda to one?
What is a higher-order function in Kotlin, and how do you pass a lambda to one?
A higher-order function takes a function as a parameter or returns one. Its parameter has a function type like (Int) -> Boolean, and callers pass a lambda, a function reference such as ::isEven, or any value of that type.
Common mistakes
- ✗Confusing a top-level function with a higher-order function
- ✗Thinking a function-type parameter must be an interface like
Runnable - ✗Forgetting that a function reference
::isEvenis itself a value of a function type
Follow-up questions
- →How does a function reference like
String::lengthdiffer from an equivalent lambda? - →What does a higher-order function that returns another function give its caller?
JuniorTheoryCommonWhich functions back the +, [] and () operators, and what does operator do?
Which functions back the +, [] and () operators, and what does operator do?
Kotlin maps each operator to a conventional function name: + calls plus, a[i] calls get, a(x) calls invoke. The function must be marked operator and fit the expected signature; without the keyword it does not compile.
Common mistakes
- ✗Omitting the
operatorkeyword and expecting+to compile anyway - ✗Naming the function after the symbol instead of the convention (
plus,get,invoke) - ✗Assuming any signature works — the convention fixes the parameters and the return type
Follow-up questions
- →Which function backs the
inoperator, and what must it return? - →How does
plusAssigndiffer frompluswhen the code writesa += b?
JuniorTheoryCommonWhat is trailing-lambda syntax, and why do Kotlin DSL calls read like blocks?
What is trailing-lambda syntax, and why do Kotlin DSL calls read like blocks?
When the last parameter of a function has a function type, the lambda may go after the parentheses: items.filter { it > 0 }. If it is the only argument the parentheses vanish, so measure { ... } reads like a language block.
Common mistakes
- ✗Trying to move a lambda out of the parentheses when it is not the last parameter
- ✗Believing the syntax requires the function to be
inline - ✗Forgetting that with a single lambda argument the parentheses disappear entirely
Follow-up questions
- →Why do library authors put the function-type parameter last in the signature?
- →How do you pass two lambdas to one function when only one may trail?
JuniorTheoryOccasionalWhat is a tail call, and why does deep recursion overflow the call stack?
What is a tail call, and why does deep recursion overflow the call stack?
A call is in tail position when it is the last thing a function does — nothing is left to compute with its result. Plain recursion keeps every pending frame on the call stack, so a deep chain exhausts it and throws StackOverflowError.
Common mistakes
- ✗Calling a recursive call a tail call although its result is still multiplied or added to
- ✗Believing the JVM removes recursive frames on its own once the function returns a value
- ✗Confusing the depth of the recursion with the size of its arguments
Follow-up questions
- →Which keyword lets the Kotlin compiler reuse a single frame for a tail-recursive function?
- →Is a recursive call inside a
tryblock in tail position, and why?
MiddleTheoryOccasionalWhy can an extension property have no backing field, and how do you define one?
Why can an extension property have no backing field, and how do you define one?
Nothing is added to the class, so there is nowhere to keep a field: an extension property has no backing field. Define it with a custom get(), and optionally a set(), computed from the receiver; an initializer is a compile error.
Common mistakes
- ✗Writing
val String.x = 1and expecting the value to be stored on the string - ✗Thinking the value is cached somewhere between two reads of the property
- ✗Believing only a
varextension property is forbidden a backing field
Follow-up questions
- →How would you give an extension property a
set()when there is no field to write into? - →What is the cost of a
get()that recomputes a heavy value on every read?
MiddleDebuggingOccasionalAn Urgent notification still renders as a plain one — find and fix the bug
An Urgent notification still renders as a plain one — find and fix the bug
An extension resolves on the declared static type, never the runtime type: it compiles to a static function and overrides nothing, so it: Notification always picks the Notification versions. Make both open members and override them.
Common mistakes
- ✗Expecting an extension to be virtual and to override a version on the base class
- ✗Reading the receiver as the runtime type of the value rather than its declared type
- ✗Marking the class
openand expecting that to change how the extension resolves
Follow-up questions
- →What would you do instead if
Notificationcame from a library you cannot modify? - →Which version wins when a member function and an extension share one signature?
MiddleTheoryOccasionalWhat is a lambda with a receiver, and how does it make a builder DSL possible?
What is a lambda with a receiver, and how does it make a builder DSL possible?
Its type is T.() -> Unit: inside the body the receiver is this, so the receiver members are in scope unqualified. A builder creates a fresh T and passes it to such a lambda, which is why html { body { ... } } names no object.
Common mistakes
- ✗Confusing
T.() -> Unitwith(T) -> Unit, where the object arrives asit - ✗Assuming the receiver only works because the builder function is
inline - ✗Losing track of which receiver an unqualified call belongs to in nested blocks
Follow-up questions
- →Which scope functions are built on a lambda with a receiver, and which are not?
- →How does the annotation
@DslMarkerchange name resolution in nested builder blocks?
MiddleTheoryOccasionalHow does a local function differ from a lambda stored in a variable?
How does a local function differ from a lambda stored in a variable?
A local function is a named function declared inside another: it reads the enclosing parameters and locals directly and needs no function object per call. A lambda in a variable is a value of a function type — an object you can pass on and return.
Common mistakes
- ✗Thinking a local function is just a lambda with a name
- ✗Assuming a local function can be passed to a higher-order function as a value without
:: - ✗Believing a lambda in a variable cannot outlive the function that created it
Follow-up questions
- →How would you pass a local function where a value of a function type is expected?
- →When is extracting a private member function better than keeping a local one?
MiddleDebuggingOccasionalThe call sites grid[1, 0] and 3 in grid do not compile — find and fix the bug
The call sites grid[1, 0] and 3 in grid do not compile — find and fix the bug
The operator form resolves only to a conventionally named function marked operator. Rename at to get and has to contains and mark both operator: an extension may carry the keyword, so the library class stays untouched.
Common mistakes
- ✗Assuming an operator must be a member and cannot be an extension
- ✗Keeping a free-form name such as
atorhasinstead of the convention - ✗Forgetting that
containsmust returnBooleanforinto work
Follow-up questions
- →What does the compiler do when the class already has a member
getwith the same signature? - →Which convention would you add so that
for (cell in grid)works as well?
MiddlePerformanceOccasionalHow does tailrec remove the stack cost, and when does the compiler not apply it?
How does tailrec remove the stack cost, and when does the compiler not apply it?
tailrec makes the compiler rewrite a self-call in tail position into a loop, so one frame is reused and deep recursion cannot overflow. n * fact(n - 1) is not in tail position: carry the pending work in an accumulator inside a local helper.
Common mistakes
- ✗Marking a function
tailrecwhile its recursive result is still multiplied or added to - ✗Expecting a compile error rather than a warning when the call is not in tail position
- ✗Believing
tailrecenlarges the stack instead of removing the recursion
Follow-up questions
- →Why is a recursive call inside a
try/catchblock never in tail position? - →What does the accumulator parameter of the local helper hold on each iteration?
SeniorTheoryOccasionalHow do Kotlin extensions differ from Java static utility methods such as the helper class StringUtils?
How do Kotlin extensions differ from Java static utility methods such as the helper class StringUtils?
They compile to the same thing: a static method taking the receiver as its first parameter, in a file class such as StringUtilsKt. What differs is the call site — s.shout() rather than shout(s) — plus extension properties.
Common mistakes
- ✗Believing an extension is dispatched virtually while a static helper is not
- ✗Expecting an extension to see the receiver's
privatestate the way a member does - ✗Forgetting that Java code calls an extension as a static method of the file class
Follow-up questions
- →How does
@JvmNameon a file change how Java code calls your extensions? - →When is a real member function the honest choice over an extension?
SeniorDesignRareYour team ships a Kotlin configuration DSL: nested builder blocks written as lambdas with a receiver, extension functions declared on those receivers, and two overloaded operators — unary + adds a dependency and - excludes one. New hires report that the call sites are unreadable: inside a nested block a bare implementation(...) could belong to any of three enclosing receivers, an unqualified name may resolve to an extension imported from an unrelated file, and jumping to a declaration lands far from the DSL. The team lead wants to keep the DSL and its conciseness. What does the implicit-receiver magic cost a reader, and how would you keep the DSL while making every call site explainable?
Your team ships a Kotlin configuration DSL: nested builder blocks written as lambdas with a receiver, extension functions declared on those receivers, and two overloaded operators — unary + adds a dependency and - excludes one. New hires report that the call sites are unreadable: inside a nested block a bare implementation(...) could belong to any of three enclosing receivers, an unqualified name may resolve to an extension imported from an unrelated file, and jumping to a declaration lands far from the DSL. The team lead wants to keep the DSL and its conciseness. What does the implicit-receiver magic cost a reader, and how would you keep the DSL while making every call site explainable?
Implicit receivers hide the subject of every call: an unqualified name may come from any enclosing receiver and binds by declared type, so a reader cannot resolve it locally. Keep the DSL but bound it: @DslMarker, few operators, one entry point.
Common mistakes
- ✗Assuming the innermost receiver always wins, when an outer one is equally in scope
- ✗Treating readability as free because the IDE resolves the name for you
- ✗Adding operators for their own sake, so the call site no longer names the action
Follow-up questions
- →How exactly does the annotation
@DslMarkerrestrict which receivers stay implicit? - →Where would you draw the line between an operator and a plainly named function?