1. Overview

Kotlin, with its concise syntax and powerful features, offers various ways to achieve efficiency and readability. One such feature is the ability to pass functions as parameters to other functions, known as higher-order functions. This capability allows for more flexible and reusable code, allowing us to write cleaner and more expressive programs.

In this tutorial, we’ll briefly discuss Kotlin’s high-order functions and explore how to pass functions as parameters to high-order functions.

2. A Few Words About High-Order Functions

In Kotlin, functions are first-class citizens, which means they can be treated as values. This includes passing functions as parameters to other functions, returning functions from functions, and assigning functions to variables.

Functions that accept other functions as parameters or return them are called higher-order functions.

Next, let’s see an example:

fun joinByOperation(theList: List<String>, operation: (List<String>) -> String): String {
    return operation(theList)
}

The joinByOperation() function accepts two parameters. The first is a list of String values, and the other is a function that turns a list of String values into a String.

Next, let’s see how to pass functions as parameters to joinByOperation().

3. Passing Lambda Expressions as Functions

Lambda expressions are actually anonymous functions in Kotlin. Therefore, we can pass a lambda expression as a function to joinByOperation() like this:

joinByOperation(aStringList, { lambda expression })

Additionally, when a function’s final parameter is a function, Kotlin allows us to place the lambda expression outside the parentheses:

joinByOperation(aStringList){ lambda expression }

This is known as trailing lambda.

Next, let’s see how to pass a lambda to the function:

val input = listOf("a b c", "d e f", "x y z")
val result1 = joinByOperation(input) { theList ->
    theList.joinToString(separator = " ") { str -> str.reversed() }.replace(" ", ", ")
}
assertEquals("c, b, a, f, e, d, z, y, x", result1)

As the code above shows, we passed a lambda to reverse each String in the list, then join them, and finally, prepend a comma to every space.

One advantage of high-order functions is their flexibility. For example, we can pass another lambda expression to ask joinByOperation() to join the list of String values reversely and convert the result String to uppercase:

val result2 = joinByOperation(input) { theList ->
    theList.reversed().joinToString(separator = " ") { str -> str }.uppercase()
}
assertEquals("X Y Z D E F A B C", result2)

As we can see, passing lambda expressions as parameters to high-order functions is straightforward and flexible.

4. Passing Existing Functions

It’s pretty convenient to pass lambdas to high-order functions. However, one drawback is that those lambdas cannot be reused outside the high-order functions.

In practice, we often define functions to reuse the logic. A function can be defined in various scopes in Kotlin, such as instance function, top-level function, etc.,

Next, let’s see how to pass existing functions as parameters to our joinByOperation() high-order function.

4.1. Passing Instance Functions

Let’s say, we have the MessageParser class with a function in our application:

class MessageParser {
    fun joinWithoutPlaceholder(segments: List<String>): String {
        return segments.joinToString(separator = " ").replace(" [SPACE] ", " ")
    }
}

As the code above shows, joinWithoutPlaceholder() is a typical instance function. It joins a list of String values and replaces the ” [SPACE] “ placeholders with a space.

When our input contains placeholders, we would like to use the joinWithoutPlaceholder() to join String segments. We can reference instance function using the instanceVariable::FunctionName format, and pass the function reference as the parameter to the high-order function:

val messageParser = MessageParser()
val input = listOf("a [SPACE] b [SPACE] c", "d [SPACE] e [SPACE] f", "x [SPACE] y [SPACE] z")
val result = joinByOperation(input, messageParser::joinWithoutPlaceholder)
assertEquals("a b c d e f x y z", result)

4.2. Passing Functions in a Class’s Companion Object

In Kotlin, there is no “static” keyword. However, we can create a static function” by declaring the function in a class’s companion object.

Next, let’s add a companion object function to the MessageParser class:

class MessageParser {
    // ...
    companion object {
        fun simplyJoin(segments: List<String>): String {
            return segments.joinToString(separator = " ")
        }
    }
}

The simplyJoin() function joins the segments in the given String list separated by a space. When we pass this companion object function as a parameter, we reference it as MessageParser::simplyJoin, using the format ClassName::FunctionName. 

Now, let’s see if it does the job:

val input = listOf("a b c", "d e f", "x y z")
val result = joinByOperation(input, MessageParser::simplyJoin)
assertEquals("a b c d e f x y z", result)

4.3. Passing Object Functions

Apart from instance functions and companion object functions, we can declare functions in a Kotlin object:

object ParserInObject {
    fun joinWithoutComma(segments: List<String>): String {
        return segments.joinToString(separator = " ") { it.replace(",", "") }
    }
}

As this example shows, the jo**inWithoutComma() function in ParserInObject joins a list of String values and removes all commas. So, when we want to skip all commas from the input, we may use this function.

To pass joinWithoutComma() as the parameter to joinByOperation(), we reference it using the ObjectName::FunctionName format. 

Next, let’s create a test to verify if we can get the expected result in this way:

val input = listOf("a, b, c", "d, e, f", "x, y, z")
val result = joinByOperation(input, ParserInObject::joinWithoutComma)
assertEquals("a b c d e f x y z", result)

4.4. Passing Top-Level Functions

Finally, in Kotlin, functions can be declared at the top level, also known as package-level functions. Next, let’s create a top-level function:

fun decrypt(segments: List<String>): String {
    return segments.reversed().joinToString(separator = " ") { it.reversed() }
}

decrypt() is defined outside a class.** It takes a list of String segments and decrypts the input by reversing each segment and the order of the segments in the input list.

We can pass a top-level function as a parameter following the ::FunctionName pattern. In our example, it will be ::decrypt.

Next, let’s pass decrypt() to joinByOperation():

val input = listOf("z y x", "f e d", "c b a")
val result = joinByOperation(input, ::decrypt)
assertEquals("a b c d e f x y z", result)

5. Assigning a Function to a Variable

Up to this point, we’ve explored two methods of passing a function as a parameter to a higher-order function in Kotlin: employing a lambda or a function reference. As previously highlighted, functions are treated as first-class citizens in Kotlin. Consequently, we can assign a function reference or a lambda expression to a variable, allowing us to pass the variable to another function.

Next, we’ll delve into understanding this concept through examples.

Let’s use the object function joinWithoutComma() and a lambda expression to demonstrate assigning existing functions and lambdas to variables. Subsequently, we’ll pass these variables to the joinByOperation() function.

val input = listOf("a, b, c", "d, e, f", "x, y, z")
 
val funRef = ParserInObject::joinWithoutComma
val resultFunRef = joinByOperation(input, funRef)
assertEquals("a b c d e f x y z", resultFunRef)
 
val funLambda = { theList: List<String> -> theList.reversed().joinToString(separator = ", ") { str -> str }.uppercase() }
val resultFunLambda = joinByOperation(input, funLambda)
assertEquals("X, Y, Z, D, E, F, A, B, C", resultFunLambda)

As we can see in the example above, Kotlin allows us to treat lambdas and function references as regular values. For example, we can assign them to variables directly. When we pass these variables as parameters to other functions, we effectively pass functions as parameters.

6. Conclusion

In this article, we first explored the concept of high-order functions in Kotlin. By embracing higher-order functions, we can improve code reusability, flexibility, and readability, leading to more maintainable software.

Subsequently, we delved into passing functions as parameters to a higher-order function through examples. We examined two scenarios: passing lambda expressions and passing references to existing functions.

Furthermore, we demonstrated that we can directly assign lambda expressions and function references to variables, enabling us to pass these variables as parameters to other functions.

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