How to Use a Kotlin Lambda Expression: Syntax, Use Cases, and Compiler Internals
A kotlin lambda expression is an anonymous function that you pass as a value to higher-order functions, store in variables, or use for SAM conversions, with the compiler representing it as KtLambdaExpression in the PSI and FirAnonymousFunction in the FIR.
Kotlin lambda expressions are the backbone of functional programming in the language, enabling concise syntax for passing behavior as data. According to the JetBrains/kotlin source code, the compiler tracks these expressions through multiple intermediate representations, from the initial parse tree to JVM bytecode. Understanding when and how to use a kotlin lambda expression helps you write idiomatic, performant code that leverages higher-order functions and DSLs.
What Is a Kotlin Lambda Expression?
A kotlin lambda expression is an anonymous function—literally a function without a name—that can be treated as a value. You can assign it to variables, pass it as arguments, or return it from other functions.
The syntax follows the pattern:
{ parameters -> body }
For example:
val sum: (Int, Int) -> Int = { a, b -> a + b }
In the JetBrains/kotlin compiler, lambdas are distinguished from anonymous functions by the isLambda flag in FirAnonymousFunction.kt at compiler/fir/tree/gen/org/jetbrains/kotlin/fir/declarations/FirAnonymousFunction.kt. While both use the same FIR node, lambdas use the shorthand syntax without the fun keyword.
Compiler Representation and Internals
The kotlin compiler processes lambda expressions through several layers, each with distinct responsibilities.
PSI Layer: KtLambdaExpression
At the parsing stage, the compiler creates a KtLambdaExpression PSI element defined in compiler/psi/psi-api/src/org/jetbrains/kotlin/psi/KtLambdaExpression.java. This node wraps a KtFunctionLiteral that contains the parameter list, optional return type, and body block.
FIR Layer: FirAnonymousFunction
During semantic analysis, the PSI converts to a FirAnonymousFunction in the FIR (Frontend Intermediate Representation). As noted in compiler/fir/tree/gen/org/jetbrains/kotlin/fir/declarations/FirAnonymousFunction.kt, this node handles both lambdas and anonymous functions, using the isLambda boolean to differentiate them. The typeRef property holds the inferred functional type derived from the call site context.
IR and Bytecode Generation
In the backend, the compiler generates a synthetic IrFunction for the lambda body. The compilation strategy depends on context:
- Inlining: When the lambda passes to an
inlinehigher-order function, the compiler substitutes the lambda body directly at the call site, eliminating object allocation and enabling non-local returns. - Invokedynamic: With the
-Xindy-allow-annotated-lambdascompiler flag (defined incompiler/arguments/src/org/jetbrains/kotlin/arguments/description/JvmCompilerArguments.kt), the compiler can emitinvokedynamicinstructions for annotated lambdas, improving JVM interoperability. - SAM Conversion: When passing a lambda where a single-abstract-method (SAM) interface is expected, the compiler generates an adapter class or uses
invokedynamicto implement the interface, forwarding calls to the lambda.
Note that lambdas compiled with invokedynamic are not serializable, which affects Gradle configuration cache usage as documented in gradle-build-conventions/gradle-plugins-common/src/main/kotlin/gradle/GradleCommon.kt.
When to Use a Kotlin Lambda Expression
Use a kotlin lambda expression whenever you need to treat behavior as data. Specific scenarios include:
- Higher-order functions: Passing logic to functions like
map,filter, or custom utilities. - Collection transformations: Concise inline operations without boilerplate method definitions.
- DSLs and builders: Lambda with receiver syntax (
StringBuilder.() -> Unit) creates type-safe builders likebuildStringor HTML DSLs. - Asynchronous programming: Coroutine builders like
launchandasyncaccept suspending lambdas that capture surrounding scope. - Event listeners: SAM conversion lets you replace verbose anonymous classes with concise lambdas for Android or JavaFX callbacks.
- Performance-critical paths: Combine with
inlinefunctions to eliminate allocation overhead and enable non-local returns.
How to Write Kotlin Lambda Expressions
Basic Syntax
Declare a lambda by enclosing parameters and body in braces:
val sum: (Int, Int) -> Int = { a, b -> a + b }
If the compiler can infer the type, omit the explicit signature:
val sum = { a: Int, b: Int -> a + b }
Trailing Lambda Syntax
When a lambda is the last argument to a function, move it outside the parentheses:
listOf(1, 2, 3).map { it * 2 }
If it’s the only argument, omit the parentheses entirely:
run { println("Executing") }
Lambda with Receiver
Use the syntax ReceiverType.() -> ReturnType to create DSLs where the lambda body executes in the context of a receiver object:
buildString {
append("Hello, ")
appendLine("World!")
}
Here, append and appendLine resolve against the implicit StringBuilder receiver.
SAM Conversion
Pass a lambda where a Java single-abstract-method interface is expected:
val runnable = Runnable { println("Running") }
button.setOnClickListener { view ->
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
The compiler generates the adapter class automatically, or uses invokedynamic when enabled.
Inline Higher-Order Functions
Mark functions that accept lambdas as inline to eliminate runtime overhead:
inline fun <T> measureTime(block: () -> T): T {
val start = System.nanoTime()
val result = block()
println("Took ${System.nanoTime() - start} ns")
return result
}
val result = measureTime {
// expensive computation
}
Inlining enables non-local returns (return from the enclosing function) and avoids allocating the lambda object.
Common Pitfalls and Diagnostics
The compiler provides specific diagnostics to help you avoid mistakes with lambdas. In compiler/frontend/src/org/jetbrains/kotlin/diagnostics/rendering/DefaultErrorMessages.java, the UNUSED_LAMBDA_EXPRESSION warning flags lambdas that are created but never invoked, often indicating a logic error where you forgot to call the function or assign the result.
Other issues to watch for:
- Variable capture: Lambdas capture final or effectively final variables from the enclosing scope. Modifying captured variables requires wrapping them in a mutable holder or using
AtomicReference. - Return ambiguity: Inside a lambda,
returnreturns from the lambda itself (if labeled) or from the enclosing function (if used in an inline higher-order function). Unlabeled returns in non-inline lambdas are prohibited.
Summary
- A kotlin lambda expression is an anonymous function treated as a value, represented in the compiler by
KtLambdaExpression(PSI) andFirAnonymousFunction(FIR). - Use lambdas for higher-order functions, collection transformations, DSLs, coroutines, and SAM conversions with Java interfaces.
- Apply trailing-lambda syntax to improve readability when the lambda is the final argument.
- Use lambda with receiver (
T.() -> R) to create type-safe builders and DSLs. - Mark higher-order functions as inline to eliminate allocation overhead and enable non-local returns.
- Be aware of SAM conversion mechanics and the
-Xindy-allow-annotated-lambdasflag forinvokedynamicgeneration, noting that such lambdas are not serializable.
Frequently Asked Questions
What is the difference between a lambda expression and an anonymous function in Kotlin?
Both are function literals without names, but lambdas use the concise syntax { params -> body } while anonymous functions use the fun keyword: fun(params): ReturnType { body }. In the compiler’s FIR representation (FirAnonymousFunction.kt), the isLambda flag distinguishes them—lambdas infer return types automatically and cannot specify explicit return types, whereas anonymous functions can.
When should I mark a higher-order function as inline with lambda parameters?
Use inline when the lambda is used frequently in performance-critical code, when you need non-local returns (returning from the enclosing function from within the lambda), or when you want to avoid the memory allocation of a function object. The compiler substitutes the lambda body directly at the call site, as seen in the IR generation phase where synthetic functions are inlined rather than invoked.
How does SAM conversion work with Kotlin lambdas?
When you pass a lambda where a Java single-abstract-method (SAM) interface is expected, the compiler either generates a synthetic adapter class that implements the interface and forwards to your lambda, or uses an invokedynamic instruction if the -Xindy-allow-annotated-lambdas flag is enabled. This allows you to write { println("done") } instead of verbose object : Runnable { override fun run() {...} } syntax.
Can Kotlin lambdas be serialized?
Standard lambdas compiled to anonymous classes can be serialized if they implement Serializable, but lambdas compiled using invokedynamic (enabled via compiler flags like -Xindy-allow-annotated-lambdas) are not serializable. As noted in gradle-build-conventions/gradle-plugins-common/src/main/kotlin/gradle/GradleCommon.kt, this distinction is critical for Gradle configuration cache compatibility and distributed computing frameworks that require serialized closures.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →