Lambdas are everywhere in Kotlin. We see them in code. They’re mentioned in the documentation and in blog posts. It’s difficult to write, read, or learn Kotlin without quickly bumping into the concept of lambdas.
But what exactly are lambdas?
If you’re new to the language or haven’t looked that closely at lambdas themselves, the concept might be confusing at times.
In this post, we’ll dive into Kotlin’s lambdas. We’ll explore what they are, how they are structured, and where they can be used. By the end of this post, you should have a complete understanding of what is, and isn’t, a lambda in Kotlin — and how to use them pragmatically for any kind of Kotlin development.
Let’s start with the formal definition.
Lambdas are a type of function literal, meaning they are a function defined without using the fun
keyword and are used immediately as part of an expression.
Because lambdas are not named or declared using the fun
keyword, we are free to easily assign them to variables or pass them as function parameters.
Let’s take a look at a few examples to help illustrate this definition. The following snippet demonstrates the use of two different lambdas in variable assignment expressions.
val lambda1 = { println("Hello Lambdas") } val lambda2 : (String) -> Unit = { name: String -> println("My name is $name") }
For both of these cases, everything to the right-hand side of the equals sign is the lambda.
Let’s look at another example. This snippet demonstrates the use of a lambda as a function argument.
// create a filtered list of even values val vals = listOf(1, 2, 3, 4, 5, 6).filter { num -> num.mod(2) == 0 }
In this case, everything after the call to .filter
is the lambda.
Sometimes, lambdas can be confusing because they can be written and used in different ways, making it difficult to understand if something is a lambda or not. An example of this can be seen in the next snippet:
val vals = listOf(1, 2, 3, 4, 5, 6).filter({ it.mod(2) == 0 })
This example shows an alternative version of the previous example. In both cases, a lambda is passed to the filter()
function. We’re going to discuss the reasons behind these differences as we progress through this post.
Now that we’ve seen a few examples of what lambdas are, it might be helpful to call out a few examples of what lambdas are not.
Lambdas are not class or function bodies. Take a look at the following class definition.
class Person(val firstName: String, val lastName: String) { private val fullName = "$firstName $lastName" fun printFullName() { println(fullName) } }
In this code, there are two sets of curly braces that look very much like lambdas. The class body is contained with a set of { }
, and the printFullName()
method’s implementation includes a method body within a set of { }
.
While these do look like lambdas, they aren’t. We’ll explore the explanation in more detail as we continue, but the basic explanation is that the curly braces in these instances aren’t representing a function expression; they’re simply part of the basic syntax of the language.
Here’s one last example of what a lambda isn’t.
val greeting = if(name.isNullOrBlank()) { "Hello you!" } else { "Hello $name" }
In this snippet, we once again have two sets of curly braces. But, the bodies of the conditional statements don’t represent a function, so they are not lambdas.
Now that we’ve seen a few examples, let’s take a closer look at the formal syntax of a lambda.
We’ve already seen that lambdas can be expressed in a few different ways. However, all lambdas do follow a specific set of rules detailed as part of Kotlin’s lambda expression syntax.
That syntax includes the following rules:
Unit
, the final expression of the lambda body is treated as the return valueit
reference->
While these rules do outline how to write and use a lambda, they can be confusing on their own without examples. Let’s look at some code that illustrates this lambda expression syntax.
The most simple lambda we could define would be something like this.
val simpleLambda : () -> Unit = { println("Hello") }
In this case, simpleLambda
is a function that takes no arguments and returns Unit
. Because there are no argument types to declare, and the return value may be inferred from the lambda body, we may simplify this lambda even further.
val simpleLambda = { println("Hello") }
Now we are relying on Kotlin’s type inference engine to infer that simpleLambda
is a function that takes no arguments and returns Unit
. The Unit
return is inferred by the fact that the last expression of the lambda body, the call to println()
, returns Unit
.
The following code snippet defines a lambda that takes two String
arguments and returns a String
.
val lambda : (String, String) -> String = { first: String, last: String -> "My name is $first $last" }
This lambda is verbose. It includes all optional type information. Both the first and last parameters include their explicit type information. The variable also explicitly defines the type information for the function expressed by the lambda.
This example could be simplified in a couple of different ways. The following code shows two different ways in which the type information for the lambda may be made less explicit by relying on type inference.
val lambda2 = { first: String, last: String -> "My name is $first $last" } val lambda3 : (String, String) -> String = { first, last -> "My name is $first $last" }
In the lambda2
example, the type information is inferred from the lambda itself. The parameter values are explicitly annotated with the String
type while the final expression can be inferred to return a String
.
For lambda3
, the variable includes the type information. Because of this, the parameter declarations of the lambda can omit the explicit type annotations; first
and last
will both be inferred as String
types.
Once you’ve defined a lambda expression, how can you invoke the function to actually run the code defined in the lambda body?
As with most things in Kotlin, there are multiple ways for us to invoke a lambda. Take a look at the following examples.
val lambda = { greeting: String, name: String -> println("$greeting $name") } fun main() { lambda("Hello", "Kotlin") lambda.invoke("Hello", "Kotlin") } // output Hello Kotlin Hello Kotlin
In this snippet, we’ve defined a lambda that will take two Strings
and print a greeting. We are able to invoke that lambda in two ways.
In the first example, we invoke the lambda as if we were calling a named function. We add parentheses to the variable name
, and pass the appropriate arguments.
In the second example, we use a special method available to functional types invoke()
.
In both cases, we get the same output. While you may use either option to call your lambda, calling the lambda directly without invoke()
results in less code and more clearly communicates the semantics of calling a defined function.
In the previous section, we briefly touched on returning values from a lambda expression. We demonstrated that the return value of a lambda is provided by the last expression within the lambda body. This is true whether returning a meaningful value or when returning Unit
.
But what if you want to have multiple return statements within your lambda expression? This is not uncommon when writing a normal function or method; do lambdas support this same concept of multiple returns?
Yes, but it’s not as straightforward as adding multiple return statements to a lambda.
Let’s look at what we might expect to be the obvious implementation of multiple returns within a lambda expression.
val lambda = { greeting: String, name: String -> if(greeting.length < 3) return // error: return not allowed here println("$greeting $name") }
In a normal function, if we wanted to return early, we could add a return
that would return out of the function before it ran to completion. However, with lambda expressions, adding a return
in this way results in a compiler error.
To accomplish the desired result, we must use what is referred to as a qualified return. In the following snippet, we’ve updated the previous example to leverage this concept.
val lambda = greet@ { greeting: String, name: String -> if(greeting.length < 3) return@greet println("$greeting $name") }
There are two key changes in this code. First, we’ve labeled our lambda by adding greet@
before the first curly brace. Second, we can now reference this label and use it to return from our lambda to the outer, calling function. Now, if greeting < 3
is true
, we will return from our lambda early and never print anything.
You may have noticed that this example doesn’t return any meaningful value. What if we wanted to return a String
rather than printing a String
? Does this concept of a qualified return still apply?
Again, the answer is yes. When making our labeled return
, we can provide an explicit return value.
val lambda = greet@ { greeting: String, name: String -> if(greeting.length < 3) return@greet "" "$greeting $name" }
The same concept can be applied if we need to have more than two returns.
val lambda = greet@ { greeting: String, name: String -> if(greeting.length < 3) return@greet "" if(greeting.length < 6) return@greet "Welcome!" "$greeting $name" }
Notice that while we now have multiple return
statements, we still do not use an explicit return
for our final value. This is important. If we added a return
to our final line of the lambda expression body, we would get a compiler error. The final return value must always be implicitly returned.
We’ve now seen many usages of parameters being used within a lambda expression. Much of the flexibility in how lambdas are written come from the rules around working with parameters.
Let’s start with the simple case. If we do not need to pass anything to our lambda, then we simply don’t define any parameters for the lambda as in the following snippet.
val lambda = { println("Hello") }
Now, let’s say we want to pass a greeting to this lambda. We’ll need to define a single String
argument:
val lambda = { greeting: String -> println("Hello") }
Notice that our lambda has changed in several ways. We now have defined a greeting
parameter within the curly braces and a ->
operator separating the parameter declarations and the body of the lambda.
Because our variable includes the type information for the parameters, our lambda expression can be simplified.
val lambda: (String) -> Unit = { greeting -> println("Hello") }
The greeting
parameter within the lambda doesn’t need to specify the type of String
because it’s inferred from the left-hand side of the variable assignment.
You might have noticed that we aren’t using this greeting
parameter at all. This sometimes happens. We might need to define a lambda that takes in an argument, but because we don’t use it, we’d like to just ignore it, saving us code and removing some complexity from our mental model.
To ignore or hide the unused greeting
parameter, we can do a couple of things. Here, we hide it by removing it altogether.
val lambda: (String) -> Unit = { println("Hello") }
Now, just because the lambda itself doesn’t declare or name the argument doesn’t mean it’s not still a part of the signature of the function. To invoke lambda
, we would still have to pass a String
to the function.
fun main() { lambda("Hello") }
If we wanted to ignore the parameter but still include it so it’s more clear that there is information being passed to the lambda invocation, we have another option. We can replace the names of unused lambda parameters with an underscore.
val lambda: (String) -> Unit = { _ -> println("Hello") }
While this looks a bit odd when used for a simple parameter, it can be quite helpful when there are multiple parameters to consider.
How do we access and use the parameter values passed to a lambda invocation? Let’s return to one of our earlier examples.
val lambda: (String) -> Unit = { println("Hello") }
How can we update our lambda to use the String
that will be passed to it? To accomplish this, we can declare a named String
parameter and work with it directly.
val lambda: (String) -> Unit = { greeting -> println(greeting) }
Now, our lambda will print whatever is passed to it.
fun main() { lambda("Hello") lambda("Welcome!") lambda("Greetings") }
While this lambda is very easy to read, it may be more verbose than some want to write. Because the lambda only has a single parameter, and that parameter’s type can be inferred, we can reference the passed String
value using the name it
.
val lambda: (String) -> Unit = { println(it) }
You’ve likely seen Kotlin code that is referencing some it
parameter that isn’t explicitly declared. This is common practice in Kotlin. Use it
when it’s extremely clear what the parameter value represents. In many cases, even if it’s less code to use the implicit it
, it’s better to name the lambda parameter so the code is easier to understand by those reading it.
Our examples so far have used a single parameter value passed to a lambda. But what if we have multiple parameters?
Thankfully, most of the same rules still apply. Let’s update our example to take both a greeting
and a thingToGreet
.
val lambda: (String, String) -> Unit = { greeting, thingToGreet -> println("$greeting $thingToGreet") }
We can name both parameters and access them within the lambda, just the same as with a single parameter.
If we want to ignore one, or both, parameters, we must rely on the underscore naming convention. With multiple parameters, we cannot omit the parameter declarations.
val lambda: (String, String) -> Unit = { _, _ -> println("Hello there!") }
If we want to ignore only one of the parameters, we are free to mix-and-match named parameters with the underscoring naming convention.
val lambda: (String, String) -> Unit = { _, thingToGreet -> println("Hello $thingToGreet") }
Destructuring lets us break an object apart into individual variables representing pieces of data from the original object. This can be very helpful in some situations, such as extracting the key
and value
from a Map
entry.
With lambdas, we take leverage destructuring when our parameter types support it.
val lambda: (Pair<String, Int>) -> Unit = { pair -> println("key:${pair.first} - value:${pair.second}") } fun main() { lambda("id123" to 5) } // output // key:id123 - value:5
We pass a Pair<String, Int>
as a parameter to our lambda, and within that lambda, we must then access the first
and second
property of the pair by referencing the Pair
first.
With destructuring, rather than declaring a single parameter to represent the passed Pair<String, Int>
, we can define two parameters: one for the first
property and one for the second
property.
val lambda: (Pair<String, Int>) -> Unit = { (key, value) -> println("key:$key - value:$value") } fun main() { lambda("id123" to 5) } // output // key:id123 - value:5
This gives us direct access to the key
and value
which saves code and may also reduce some of the mental complexity. When all we care about is the underlying data, not having to reference the containing object is one less thing to think about.
For more on the rules around destructuring, whether for variables or lambdas, check out the official documentation.
We’ve now seen how to work with values passed directly to our lambdas. However, a lambda can also access data from outside its definition.
Lambdas can access data and functions from outside their scope. This information from the outer scope is the lambda’s closure. The lambda can call functions, update variables, and use this information however it needs.
In the following example, the lambda accesses a top-level property currentStudentName
.
var currentStudentName: String? = null val lambda = { val nameToPrint = currentStudentName ?: "Our Favorite Student" println("Welcome $nameToPrint") } fun main() { lambda() // output: Welcome Our Favorite Student currentStudentName = "Nate" lambda() // output: Welcome Nate }
The two invocations of lambda()
in this case result in different outputs. This is because each invocation will use the current value of currentStudentName
.
So far, we’ve been assigning lambdas to variables and then invoking those functions directly. But what if we need to pass our lambda as a parameter of another function?
In the following example, we’ve defined a higher-order function called processLangauges
.
fun processLanguages(languages: List<String>, action: (String) -> Unit) { languages.forEach(action) } fun main() { val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust") val action = { language: String -> println("Hello $language") } processLanguages(languages, action) }
The processLanguages
function takes a List<String>
and also a function parameter which itself takes a String
and returns Unit
.
We’ve assigned a lambda to our action
variable, and then pass action
as an argument when invoking processLanguages
.
This example demonstrates that we can pass a variable storing a lambda to another function.
But what if we didn’t want to assign the variable first? Can we pass a lambda directly to another function? Yes, and it’s common practice.
The following snippet updates our previous example to pass the lambda directly to the processLanguages
function.
fun processLanguages(languages: List<String>, action: (String) -> Unit) { languages.forEach(action) } fun main() { val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust") processLanguages(languages, { language: String -> println("Hello $language") }) }
You’ll see that we no longer have the action
variable. We are defining our lambda at the point where it’s passed as an argument to the function invocation.
Now there’s one issue with this. The resulting call to processLanguages
is hard to read. Having a lambda defined within the parentheses of a function call is a lot of syntactic noise for our brains to parse through when reading code.
To help deal with this, Kotlin supports a specific kind of syntax referred to as trailing lambda syntax. This syntax states that if the final parameter to a function is another function, then the lambda can be passed outside of the function call parentheses.
What does that look like in practice? Here’s an example:
fun main() { val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust") processLanguages(languages) { language -> println("Hello $language") } }
Notice that the call to processLanguages
now has only one value passed to the parentheses, but now has a lambda directly after those parentheses.
The use of this trailing lambda syntax is extremely common with the Kotlin Standard Library.
Take a look at the following example.
fun main() { val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust") languages.forEach { println(it) } languages .filter { it.startsWith("K")} .map { it.capitalize() } .forEach { println(it) } }
Each of these calls to forEach
, map
, and filter
are leveraging this trailing lambda syntax, enabling us to pass the lambda outside of the parentheses.
Without this syntax, this example would look more like this.
fun main() { val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust") languages.forEach({ println(it) }) languages .filter({ it.startsWith("K")}) .map({ it.capitalize() }) .forEach({ println(it) }) }
While this code is functionally the same as the earlier example, it begins to look much more complex as the parentheses and curly braces add up. So, as a general rule, passing lambdas to a function outside of the function’s parentheses improves the readability of your Kotlin code.
We’ve been exploring lambdas as a means of expressing functional types in Kotlin. One other way in which we can leverage lambdas is when performing Single Access Method (or SAM) conversions.
If you need to provide an instance of an interface with a single abstract method, SAM conversion lets us use a lambda to represent that interface rather than having to instantiate a new class instance to implement the interface.
Consider the following.
interface Greeter { fun greet(item: String) } fun greetLanguages(languages: List<String>, greeter: Greeter) { languages.forEach { greeter.greet(it) } } fun main() { val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust") greetLanguages(languages, object : Greeter { override fun greet(item: String) { println("Hello $item") } }) }
The greetLanguages
function takes an instance of a Greeter
interface. To satisfy the need, we create an anonymous class to implement Greeter
and define our greet
behavior.
This works fine, but it has some drawbacks. It requires us to declare and instantiate a new class. The syntax is verbose and makes it difficult to follow the function invocation.
With SAM conversion, we can simplify this.
fun interface Greeter { fun greet(item: String) } fun greetLanguages(languages: List<String>, greeter: Greeter) { languages.forEach { greeter.greet(it) } } fun main() { val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust") greetLanguages(languages) { println("Hello $it") } }
Notice that now the call to greetLanguages
is much easier to read. There is no verbose syntax and no anonymous class. The lambda here is now performing SAM conversion to represent the Greeter
type.
Notice also the change to the Greeter
interface. We added the fun
keyword to the interface. This marks the interface as a functional interface that will give a compiler error if you try to add more than one public abstract method. This is the magic that enables easy SAM conversion for these functional interfaces.
If you’re creating an interface with a single public, abstract method, consider making it a functional interface so you may leverage lambdas when working with the type.
Hopefully, these examples have helped shed some light on what lambdas are, how to define them, and how to work with them to make your Kotlin code more expressive and understandable.
LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your Android apps — try LogRocket for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.