Lambdas are everywhere in Kotlin. We see them in the code. They are mentioned in documents and blog posts. It is difficult to write, read, or study Kotlin without immediately encountering the concept of lambdas.

But what exactly is _lambdas?

If you’re new to the language, or haven’t studied lambdas themselves, the concept can be confusing at times.

In this article, we will delve into Kotlin’s lambdas. We’ll explore what they are, how they’re structured, and where they can be used. By the end of this article, you should have a complete understanding of lambdas in Kotlin and how to use them pragmatically in any type of Kotlin development.

What is Kotlin lambda?

Let’s start with the formal definition.

Lambdas are a _ function literal _, meaning they are a function definition that does not use the fun keyword and is immediately used as part of an expression.

Because lambdas are not named or declared using the fun keyword, we are free to assign them to variables or pass them as function arguments.

Example of lambdas in Kotlin

Let’s 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") 
}
Copy the code

In both cases, everything to the right of the equals sign is a lambda.

Let’s look at another example. This snippet demonstrates using lambda as a function parameter.

// create a filtered list of even values
val vals = listOf(1, 2, 3, 4, 5, 6).filter { num ->
    num.mod(2) == 0
}
Copy the code

In this case, everything after the.filter call is lambda.

Lambdas can sometimes be confusing because they can be written and used in different ways, making it difficult to understand whether something is a lambda or not. An example of this can be seen in the next clip.

val vals = listOf(1, 2, 3, 4, 5, 6).filter({ it.mod(2) == 0 })
Copy the code

This example shows another version of the previous example. In both cases, a lambda is passed to the filter() function. We will discuss the reasons behind these differences in this article.

What is Kotlin Lambda not

Now that we’ve seen some examples of what lambdas_ is, it might be helpful to point out some examples of what lambdas_ is not.

A lambdas is not a class or function body. Take a look at the class definition below.

class Person(val firstName: String, val lastName: String) {
    private val fullName = "$firstName $lastName"
    
    fun printFullName() {
        println(fullName)
    }
}
Copy the code

In this code, there are two sets of braces that look a lot like lambdas. The body of the class is contained in a set of {}, and the implementation of the printFullName() method is contained in a set of {}.

Although these look like lambdas, they are not. We’ll explain this in more detail in the following discussion, but the basic explanation is that braces in these instances do not represent function expressions; They are just part of the basic grammar of a language.

Here is one final example of what lambda is not.

val greeting = if(name.isNullOrBlank()) {
    "Hello you!"
} else {
    "Hello $name"
}
Copy the code

In this fragment, we once again have two sets of braces. However, the body of conditional statements does not represent a function, so they are not lambdas.

Now that we’ve seen some examples, let’s take a closer look at the formal syntax of lambda.

Understand basic lambda syntax

As we have seen, lambdas can be expressed in several different ways. However, all lambdas follow a specific set of rules, detailed as part of Kotlin’s lambda expression syntax.

This syntax includes the following rules.

  • Lambdas are always surrounded by braces
  • If lambda’s return type is notUnitThe final expression of the lambda body is treated as the return value.
  • Parameter declarations are enclosed in braces and can have optional type annotations
  • If there is a parameter, it can be used implicitly in the lambda bodyit
  • Parameter declarations and lambda bodies must be separated by a.->

While these rules 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 λ expression syntax.

Declare simple lambdas

The simplest lambda we can define looks like this.

val simpleLambda : () -> Unit = { println("Hello") }
Copy the code

In this case, simpleLambda is a function that takes no arguments and returns Unit. Since there are no parameter types to declare, and the return value can be inferred from the body of the lambda, we can simplify the lambda further.

val simpleLambda = { println("Hello") }
Copy the code

Now we rely on Kotlin’s type-reasoning engine to infer that simpleLambda is a function that takes no arguments and returns Unit. The return of Unit is inferred from the fact that the last expression of the λ body, a call to println(), returns Unit.

Declare 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"
}
Copy the code

The lambda is rough. It includes all optional type information. The _ first _ and _ last _ arguments both contain their explicit type information. This variable also explicitly defines the type information of the function λ expresses.

This example can be simplified in several different ways. The following code shows two different ways to make the type information of a lambda less explicit by relying on type reasoning.

val lambda2 = { first: String, last: String -> 
    "My name is $first $last"
}
val lambda3 : (String, String) -> String = { first, last -> 
    "My name is $first $last"
}
Copy the code

In the case of lambda2, the type information is inferred from the lambda itself. The parameter value is explicitly annotated as String, and the final expression can be inferred to return a String.

In the case of lambda3, this variable includes type information. Because of this, lambda argument declarations can omit explicit type comments; Both first and last will be inferred to be strings.

Calling lambda expressions

Once you have defined a lambda expression, how do you call this function to actually run the code defined in the lambda body?

As with most things in Kotlin, we have multiple ways to invoke lambda. Take a look at the following example.

val lambda = { greeting: String, name: String -> 
    println("$greeting $name")
}

fun main() {
    lambda("Hello", "Kotlin")
    lambda.invoke("Hello", "Kotlin")
}

// output
Hello Kotlin
Hello Kotlin
Copy the code

In this fragment, we define a lambda that will accept two Strings and print a greeting. We can call the lambda in two ways.

In the first example, we call lambda as if we were calling a named function. We parentheses the variable name and pass the appropriate parameters.

In the second example, we use invoke(), a special method available to function types.

In both cases, we get the same output. While you can use either option to call your lambda, calling lambda directly instead of invoke() results in less code and more clearly conveys the semantics of calling defined functions.

Returns a value from lambda

In the previous section, we talked briefly about returning values from lambda expressions. We show that the return value of λ is provided by the last expression in the λ body. This is true whether you return a meaningful value or Unit.

But what if you want to have multiple return statements in a lambda expression? This is not uncommon when writing ordinary functions or methods; Does Lambdas also support this concept of multiple returns?

Yes, but it’s not as straightforward as adding multiple return statements to a lambda.

Let’s take a look at the obvious multiple returns we might expect to implement in a lambda expression.

val lambda = { greeting: String, name: String -> 
    if(greeting.length < 3) return // error: return not allowed here
    
    println("$greeting $name")
}
Copy the code

In a normal function, if we want to return early, we can add a return that returns before the function is finished. However, in a λ expression, adding a return in this way causes a compiler error.

To achieve the desired result, we must use what is called a qualified return. In the following snippet, we update the previous example to take advantage of this concept.

val lambda = greet@ { greeting: String, name: String -> 
    if(greeting.length < 3) return@greet
    
    println("$greeting $name")
}
Copy the code

There are two key changes to this code. First, we mark our lambda by adding greet@ before the first curly brace. Second, we can now reference this tag and use it to return from our lambda to an external calling function. Now, if greeting < 3 is true, we will return early from our lambda and not print anything.

You may have noticed that this example does not return any meaningful values. What if we want to return a String instead of printing a String? Does this concept of return qualification still apply?

Again, the answer is yes. In making our tag return, we can provide an explicit return value.

val lambda = greet@ { greeting: String, name: String -> 
    if(greeting.length < 3) return@greet ""
    
    "$greeting $name"
}
Copy the code

If we need to have more than two returns, the same concept applies.

val lambda = greet@ { greeting: String, name: String -> 
    if(greeting.length < 3) return@greet ""
    if(greeting.length < 6) return@greet "Welcome!"
    
    "$greeting $name"
}

Copy the code

Note that although we now have multiple return statements, we still don’t use an explicit return for our final value. This is important. If we add a return to the last line of the body of the lambda expression, we get a compiler error. The final return value must always be returned implicitly.

Work with lambda parameters

We have now seen many uses of arguments in lambda expressions. Much of the flexibility in writing lambdas comes from the rules that work with the parameters.

Declaring lambda parameters

Let’s start with the easy case. If we don’t need to pass anything to our lambda, then we don’t need to define any parameters for lambda, as in the following paragraph.

val lambda = { println("Hello") }
Copy the code

Now, let’s say we want to pass a greeting to the lambda. We will need to define a single String argument.

val lambda = { greeting: String -> println("Hello") }
Copy the code

Notice that our lambda has changed in several ways. We now define a greeting parameter inside curly braces and separate the parameter declaration from the body of the lambda with a -> operator.

Because our variables include the type information of the parameters, our λ expression can be simplified.

val lambda: (String) -> Unit = { greeting -> println("Hello") }
Copy the code

The greeting parameter in lambda does not need to specify the type of String because it is inferred from the left side of the variable assignment.

You may have noticed that we don’t use the greeting parameter at all. This happens sometimes. We might need to define a lambda that takes arguments, but since we don’t use it, we want to just ignore it, saving our code and removing some of the complexity from our mental model.

To ignore or hide unused greeting parameters, there are a few things you can do. In this case, we hide it by deleting it completely.

val lambda: (String) -> Unit = { println("Hello") }
Copy the code

Now, just because lambda itself doesn’t declare or name this parameter doesn’t mean it’s not part of the function signature. To call a lambda, we still need to pass a String to the function.

fun main() {
    lambda("Hello")
}
Copy the code

If we want to ignore this argument but still include it to make it clearer that information is being passed to the lambda call, we have another option. We can replace the names of unused lambda arguments with underscores.

val lambda: (String) -> Unit = { _ -> println("Hello") }
Copy the code

While this may seem a little strange for a simple parameter, it can be quite helpful when you have multiple parameters to consider.

Accessing lambda parameters

How do we access and use the parameter values passed to lambda calls? Let’s go back to our earlier example.

val lambda: (String) -> Unit = { println("Hello") }
Copy the code

How do we update our lambda to use the String that will be passed to it? To do this, we can declare a parameter named String and use it directly.

val lambda: (String) -> Unit = { greeting -> println(greeting) }
Copy the code

Now, our lambda will print whatever is passed to it.

fun main() { lambda("Hello") lambda("Welcome!" ) lambda("Greetings") }Copy the code

While this lambda is very easy to read, it may be more verbose than some people want to write. Because the lambda has only one argument, and the type of that argument can be inferred, we can refer to the String value passed in with the name it.

val lambda: (String) -> Unit = {  println(it) }
Copy the code

You’ve probably seen Kotlin code reference some UNdeclared IT parameter. This is a common practice for Kotlin. Use it when it is clear what the parameter value represents. In many cases, even with the implied IT, there is less code, but it is best to name lambda parameters so that people reading the code can understand them more easily.

Handle multiple lambda arguments

So far, our examples have been passed to lambda using a single parameter value. But what if we have multiple parameters?

Thankfully, most of the same rules still apply. Let’s update our example and accept both a greeting and a thingToGreet.

val lambda: (String, String) -> Unit = { greeting, thingToGreet -> 
    println("$greeting $thingToGreet") 
}
Copy the code

We can name both arguments and access them in lambda as if we were using a single argument.

If we want to ignore one or two arguments, we must rely on the underline naming convention. For multiple arguments, we cannot omit the declaration of the argument.

val lambda: (String, String) -> Unit = { _, _ -> 
    println("Hello there!")
}
Copy the code

If we want to ignore only one of the arguments, we are free to mix and match arguments with the underlined naming convention.

val lambda: (String, String) -> Unit = { _, thingToGreet -> 
    println("Hello $thingToGreet") 
}
Copy the code

Lambda parameters are used for structural reorganization

Deconstruction lets us decompose an object into individual variables that represent pieces of data from the original object. Map This can be useful in some situations, such as extracting from a key and value entry.

In lambdas, we can take advantage of refactoring when our parameter types are supported.

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
Copy the code

We pass a Pair

as an argument to our lambda, in which we must first access the first and second properties of the Pair by referring to Pair.
,>

Through refactoring, we can define two parameters: one for the first attribute and one for the second attribute, rather than declaring a parameter to represent the Pair

passed.
,>

val lambda: (Pair<String, Int>) -> Unit = { (key, value) -> 
    println("key:$key - value:$value")
}

fun main() {
    lambda("id123" to 5)
}

// output
// key:id123 - value:5
Copy the code

This gives us direct access to the key and value, which saves code and reduces some of the psychological complexity. When we are concerned with the underlying data, there is one less thing to consider when we do not need to reference the contained objects.

For more rules on deconstruction, whether variables or lambdas, check out the official documentation.

Access closure data

We have now seen how to handle values passed directly to the 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 outside scope is the _ closure of lambda. Lambda can call functions, update variables, and use this information in the way it needs to.

In the following example, 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 }Copy the code

In this case, two calls to lambda() produce different output. This is because each call uses the current value of currentStudentName.

Pass the lambdas as function arguments

So far, we’ve been assigning lambdas to variables and then calling those functions directly. But what if we need to pass our lambda as an argument to another function?

In the example below, we define 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)
}
Copy the code

The processLanguages function accepts a List

as well as a function parameter that itself accepts a String and returns Unit.

We assign a lambda to our action variable and then call processLanguages with action as an argument.

This example demonstrates that we can pass a variable containing a lambda to another function.

But what if we don’t want to assign variables first? Can we just pass a lambda to another function? Yes, and it’s a common practice.

The following code snippet updates our previous example by passing 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") })
}
Copy the code

As you can see, we no longer have the action variable. We define lambda where we pass it as an argument to the function call.

Now, there’s a problem with that. The result of the call to processLanguages is hard to read. Defining a lambda within parentheses of a function call requires a lot of syntactic noise for our brains to parse when reading code.

To help solve this problem, Kotlin supports a special syntax called trailing Lamb French. This syntax states that if the final argument to a function is another function, λ may be passed _ outside the parentheses _ of the _ function call.

What does this look like in practice? Here’s an example.

fun main() {
    val languages = listOf("Kotlin", "Java", "Swift", "Dart", "Rust")
    processLanguages(languages) { language -> 
        println("Hello $language") 
    }
}
Copy the code

Notice that the call to processLanguages now has only one value passed into the parentheses, but there is now a lambda directly after those parentheses.

The use of this trailing lambda syntax is extremely common in 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) }
}
Copy the code

Every call to forEach,map, and filter takes advantage of this post-lambda syntax, allowing us to pass a lambda outside parentheses.

Without this syntax, the 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) }) }Copy the code

While this code is functionally the same as the previous example, it starts to look more complex as you add braces and braces. Therefore, as a general rule, passing the lambdas to a function outside of parentheses improves the readability of Kotlin code.

Use lambdas for SAM conversions in Kotlin

We have been exploring Lambdas as a means of expressing functional types in Kotlin. Another way we can leverage lambdas is when performing a Single Access Method (or SAM) transformation.

What is SAM transformation?

If you need to provide an interface instance with a single abstract method, the SAM transformation lets us use a lambda to represent the interface rather than having to instantiate a new class instance to implement the interface.

Consider the following situation.

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")
        }
    })
}
Copy the code

The greetLanguages function requires an instance of the Greeter interface. To meet this requirement, we create an anonymous class to implement Greeter and define our greet behavior.

This is all very well, but it has some disadvantages. It requires us to declare and instantiate a new class. The syntax is verbose, making it difficult to track function calls.

With the SAM transformation, we can simplify this problem.

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") }
}
Copy the code

Notice that the call to greetLanguages is now easier to read. There is no verbose syntax, and there are no anonymous classes. The lambda here is now performing SAM conversions to represent the type of Greeter.

Also notice changes to the Greeter interface. We added the fun keyword to the interface. This indicates that the interface is a functional interface, and if you try to add more than one public abstract method, the compiler will give you an error. This is what makes these functional interfaces easy to implement SAM transformations.

If you want to create an interface that has only one common abstract method, consider making it a functional interface so that you can take advantage of lambdas when working with this type.

conclusion

Hopefully these examples will help us understand what lambdas are, how to define them, and how to use them to make your Kotlin code more expressive and understandable.

The postA complete guide to Kotlin lambda expressionsappeared first onLogRocket Blog.