1. Overview

In Kotlin, functions are first-class. Furthermore, lambda expressions provide a concise and powerful way to express functionality. These anonymous functions allow us to write more expressive code.

However, using the return keyword inside a lambda might initially seem confusing due to the nature of lambdas and their implicit return behavior. In this tutorial, we’ll explore the return usage inside a lambda in Kotlin.

2. Introduction to the Problem

While lambdas typically rely on implicit returns, situations may arise where explicit returns are necessary for complex logic or to break out of nested structures.

Next, let’s look at an example of using return in lambda.

2.1. An Example

Let’s say we receive a string message from another system:

val input = """
    T, T, T, T, T, T
    F, O, F, O, F, O
    T, F, O, F
    T, O, T, O, T, O
    T, X, T, X, T, X
    F, F, F, F, F, F
""".trimIndent()

As we can see, the input is a multi-line string. Our task is parsing this input and extracting the valid values to the required data format.

Each line should contain exactly six comma-separated fields. We abandon the lines with more or less than six fields, such as the third line in the input.

*The valid field values are T (True), F (False), and O (Empty).* We’ll map them to the Answer enum:

enum class Answer {
    True, False, Empty
}

Due to the external system failure, some lines contain invalid fields. For example, the fifth line holds three ‘X’ letters. As long as the line has six fields, we take the valid fields and skip the invalid ones.

So, if we parsed the input example, we should get a list of lists:

val expectedResult = listOf(
    listOf(True, True, True, True, True, True),
    listOf(False, Empty, False, Empty, False, Empty),
    listOf(True, Empty, True, Empty, True, Empty),
    listOf(True, True, True),
    listOf(False, False, False, False, False, False),
)

Next, let’s write a function to parse the input.

2.2. Processing the Input

We’ll create a function to parse the input and fill a predeclared resultList variable:

lateinit var resultList: MutableList<List<Answer>>

fun processInputV1(input: String) {
    resultList = mutableListOf()
    input.lines().forEach { line ->
        val fields = line.split(", ")
        if (fields.size != 6) return
        val answerList: MutableList<Answer> = mutableListOf()
        fields.forEach { field ->
            answerList += when (field) {
                "T" -> True
                "F" -> False
                "O" -> Empty
                else -> return
            }
        }
        resultList += answerList
    }
}

In the function above, we use a nested forEach to parse the input. Further, we use the return keyword twice:

  • In the outer forEach – “if (fields.size != 6) return”  to skip the entire line if the number of fields isn’t six
  • In the inner forEachwhen(field){ … else -> return} to skip the current field if it’s not valid

Next, let’s test our function and see if it works as expected.

2.3. Testing the Function

Let’s first create a function to “pretty print” the resultList content.

fun printResult() {
    log.info(
        """
           |The Result After Processing:
           |----------------
           |${resultList.joinToString(separator = System.lineSeparator()) { "$it" }}
           |----------------
        """.trimMargin())
}

If we pass the input to our processInputV1(), it turns out the function doesn’t work as expected:

processInputV1(input)
assertNotEquals(expectedResult, resultList)
printResult()

Let’s have a look at the resultList‘s value:

The Result After Processing:
----------------
[True, True, True, True, True, True]
[False, Empty, False, Empty, False, Empty]
----------------

The output shows that we discarded the third line, which is desired. However, the whole process stopped there.

Next, let’s figure out why this happened and how to fix the problem.

3. Return to Labels

Our processing stopped at the return statement in the outer forEach. This is because the return statement makes the enclosing function processInputV1() “return”. But we want the return statement to return the current lambda to skip the current line and take the next line from the outer forEach.

So next, let’s see how to achieve it.

3.1. Return to the forEach Label

In Kotlin, when we use the return keyword in a lambda expression, we can choose whether we want to return the current lambda expression or the enclosing function using labels.

For example, if we want to return the current lambda of the outer forEach, we can use return@forEach:

fun processInputV2(input: String) {
    resultList = mutableListOf()
    input.lines().forEach { line ->
        val fields = line.split(", ")
        if (fields.size != 6) return@forEach
        val answerList: MutableList<Answer> = mutableListOf()
        fields.forEach { field ->
            answerList += when (field) {
                "T" -> True
                "F" -> False
                "O" -> Empty
                else -> return
            }
        }
        resultList += answerList
    }
}

Now, let’s parse the input with the processInputV2() function. It still doesn’t work as expected:

processInputV2(input)
assertNotEquals(expectedResult, resultList)
printResult()

Next, let’s look at where the processing stopped:

The Result After Processing:
----------------
[True, True, True, True, True, True]
[False, Empty, False, Empty, False, Empty]
[True, Empty, True, Empty, True, Empty]
----------------

The output shows we’ve skipped the third line in the input. But the return statement in the inner forEach returned the enclosing function processInputV2(). This time, we know how to fix the problem. So, let’s add the @forEach label to the return keyword:

fun processInputV3(input: String) {
    resultList = mutableListOf()
    input.lines().forEach { line ->
        val fields = line.split(", ")
        if (fields.size != 6) return@forEach
        val answerList: MutableList<Answer> = mutableListOf()
        fields.forEach { field ->
            answerList += when (field) {
                "T" -> True
                "F" -> False
                "O" -> Empty
                else -> return@forEach
            }
        }
        resultList += answerList
    }
}

Now, if we run the test, we get the expected result:

processInputV3(input)
assertEquals(expectedResult, resultList)

We might have realized that both return statements are with the @forEach label. Of course, the compiler knows which @forEach label indicates which lambda. However, when we read the code, multiple returns with the same label might not be straightforward to understand. 

Next, let’s improve that.

3.2. Return to Custom Labels

Kotlin allows us to define our own labels before a lambda following the “labelName@” format and reference defined labels in return statements. Next, let’s add two meaningful labels to our input processing function:

fun processInputV4(input: String) {
    resultList = mutableListOf()
    input.lines().forEach lineProcessing@{ line ->
        val fields = line.split(", ")
        if (fields.size != 6) return@lineProcessing
        val answerList: MutableList<Answer> = mutableListOf()
        fields.forEach answerProcessing@{ field ->
            answerList += when (field) {
                "T" -> True
                "F" -> False
                "O" -> Empty
                else -> return@answerProcessing
            }
        }
        resultList += answerList
    }
}

Finally, the processInputV4() function with the custom labels passes the test:

processInputV4(input)
assertEquals(expectedResult, resultList)

4. Conclusion

In this article, we’ve explored how to use the return keyword inside a lambda in Kotlin.

Kotlin’s return to labels feature allows us to write more flexible and expressive code, striking a balance between conciseness and control.

As always, the complete source code for the examples is available over on GitHub.