Nate Ebel Building great software and helping others do the same.

A complete guide to Kotlin lambda expressions

13 min read 3772

A Complete Guide To Kotlin Lambda Expressions

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.

What is a Kotlin lambda?

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.

Examples of lambdas in Kotlin

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.

We made a custom demo for .
No really. Click here to check it out.

// 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.

What a Kotlin lambda is not

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.

Understanding basic lambda syntax

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:

  • Lambdas are always surrounded by curly braces
  • If the return type of a lambda is not Unit, the final expression of the lambda body is treated as the return value
  • Parameter declarations go inside the curly brackets and may have optional type annotations
  • If there is a single parameter, it may be accessed within the lambda body using an implicit it reference
  • Parameter declarations and the lambda body must be separated by a ->

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.

Declaring simple lambdas

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.

Declaring complex lambdas

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.

Invoking a lambda expression

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.

Returning values from a lambda

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 = [email protected] { greeting: String, name: String -> 
    if(greeting.length < 3) [email protected]
    
    println("$greeting $name")
}

There are two key changes in this code. First, we’ve labeled our lambda by adding [email protected] 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 = [email protected] { greeting: String, name: String -> 
    if(greeting.length < 3) [email protected] ""
    
    "$greeting $name"
}

The same concept can be applied if we need to have more than two returns.

val lambda = [email protected] { greeting: String, name: String -> 
    if(greeting.length < 3) [email protected] ""
    if(greeting.length < 6) [email protected] "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.

Working with lambda arguments

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.

Declaring lambda 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.

Accessing lambda parameters

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.

Working with multiple lambda parameters

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 with lambda parameters

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.

Accessing closure data

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.

Passing lambdas as function arguments

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.

Using lambdas for SAM conversions in Kotlin

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.

What is a SAM conversion?

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.

Conclusion

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.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Nate Ebel Building great software and helping others do the same.

Leave a Reply