Kotlin core syntax: higher order functions, Lambda as parameter and return value

Keywords: Android Lambda Java iOS

Blog Homepage

1. Declare higher-order functions

A higher-order function is one that takes another function as a parameter or return value. In kotlin, functions can be represented by lambda or function references.
For example, the filter function in the standard library takes a judging function as a parameter, so it is a higher-order function

list.filter { x > 0 }

1.1 function type

In order to declare a function with lambda as an argument, you need to know how to declare the type of the corresponding parameter.

Let's start with a simple example: save a lambda expression in a local variable.

// A function with two Int type parameters and an Int type return value
val sum = {x: Int, y: Int -> x + y}

// Functions without parameters and return values
val action = { println("32") }

The compiler infers that the sum and action variables have function types. What are the explicit type declarations for these variables?

val sum: (Int, Int) -> Int = { x, y -> x + y }

val action: () -> Unit = { println("32") }

To declare a function type, you need to put the function parameter type in parentheses, followed by an arrow and the function return type. A function type declaration always requires an explicit return type, where Unit cannot be omitted.

How to omit the type of parameter x, y in lambda expression {x, Y - > x + y}? Because their types are already specified in the variable declaration of the function type, there is no need to repeat the declaration in the lambda definition.

Return values of function types can also be marked as nullable:

var canReturnNull: (Int, Int) -> Int? = { null }

You can also define a nullable variable of function type:

var funOrNull: ((Int, Int) -> Int)? = null

1.2 calling functions as parameters

Knowing how to declare higher-order functions, how to implement them?

Take an example: define a simple high-order function, realize any operation of two numbers 2 and 3, and then print the result

fun twoAndThree(operation: (Int, Int) -> Int) {
    // Parameters of call function type
    val result = operation(2, 3)
    println("The result is $result")
}

twoAndThree { i, j -> i + j }
// The result is 5
twoAndThree { i, j -> i * j }
// The result is 6

The syntax for calling a function as a parameter is the same as for calling a normal function: put parentheses after the function name, and put parameters in parentheses.

Another example: implement a simple version of filter function
The filter function is a judgment as a parameter. The judgmental type is a function that takes characters as parameters and returns a value of type boolean.

fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        // Call passed as an argument to the 'predicate' function
        if (predicate(element)) sb.append(element)
    }
    return sb.toString()
}

// Pass a lambda as the predicate parameter
println("ab3d".filter { c -> c in 'a'..'z' })
// abd

1.3 using function classes in java

The principle behind is: function type is declared as a common interface, and a variable of function type is an implementation of FunctionN interface.

A series of interfaces are defined in the kotlin standard library. These interfaces correspond to functions with different parameters, such as function0 < R > (functions without parameters), function1 < P1, R > (functions with one parameter), etc. Each interface defines an invoke method, which is called to execute the function. A function type variable is an instance of the implementation class that implements the corresponding function n interface. The invoke method of the implementation class contains the body of the lambda function.

java8's lambda is automatically converted to a function type value

// kotlin statement
fun processTheAnswer(f: (Int) -> Int) {
    println(f(34))
}

// java
processTheAnswer.(number -> number + 1);
// 35

In the old version of java, you can pass an instance of an anonymous class that implements the invoke method in the interface function:

processTheAnswer(new Function1<Integer, Integer>() {
    @Override
    public Integer invoke(Integer integer) {
        // Using function types in Java code (before java8)
        return integer + 1;
    }
});

When using the function with lambda as the parameter in the kotlin standard library in java, you must explicitly pass a receiver object as the first parameter:

List<String> list = new ArrayList<>();
list.add("23");

// You can use the functions in the kotlin standard library in java
CollectionsKt.forEach(list, s -> {
    System.out.println(s);
    // Must explicitly return a value of type Unit
    return Unit.INSTANCE;
});

1.4 parameter default value and null value of function type

For example: joinToString function with hard coded toString transformation

fun <T> Collection<T>.joinToString(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        // Use the default toString method to convert objects to strings
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}

Always use the toString method to convert elements in a collection to strings. You can define a function type parameter and use a lambda as its default value

fun <T> Collection<T>.joinToString(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = "",
    // Declare a parameter of function type with lambda as default value
    transform: (T) -> String = { it.toString() }
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        // Call the function passed as an argument to the "transform" parameter
        result.append(transform(element))
    }

    result.append(postfix)
    return result.toString()
}

val list = listOf("A", "B", "C")
// Pass a lambda as a parameter
println(list.joinToString { it.toLowerCase() })
// a, b, c

You can also declare a function type with nullable arguments. However, functions passed in as parameters cannot be called directly. kotlin will fail to compile because it detects potential null pointer exceptions. We can explicitly check null

fun foo(callback: (() -> Unit)?) {
   // ...
   if (callback != null) {
      callback()
   }
}

The function type can be used as a concrete implementation of an interface containing the invoke method.
For example: use nullable parameters of function type

fun <T> Collection<T>.joinToString(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = "",
    // Declare a nullable parameter of function type
    transform: ((T) -> String)? = null
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        // Calling functions using secure call syntax
        // Using the Elvis operator to handle cases where a callback is not specified
        val str = transform?.invoke(element) ?: element.toString()
        result.append(str)
    }

    result.append(postfix)
    return result.toString()
}

1.5 function of return function

Define a function that returns a function:

enum class Delivery {
    STANDARD, EXPEDITED
}

class Order(val itemCount: Int)

fun getShippingCostCalculator(
    delivery: Delivery
): (Order) -> Double { // Declare a function that returns a function

    if (delivery == Delivery.EXPEDITED) {
        // Return to lambda
        return { order -> 6 + 2.1 * order.itemCount }
    }

    return { order -> 1.2 * order.itemCount }
}

// Save the returned function in a variable
val calculator = getShippingCostCalculator(Delivery.EXPEDITED)
// Call the returned function
println("shipping costs ${calculator(Order(3))}")
// shipping costs 12.3

1.6 removing duplicate code by lambda

data class SiteVisit(
    val path: String,
    val duration: Double,
    val os: OS
)

enum class OS {
    IOS, ANDROID
}

First, analyze the data using a hard coded filter:

val list = listOf(
    SiteVisit("/", 22.0, OS.IOS),
    SiteVisit("/", 16.3, OS.ANDROID)
)

val averageIOSDuration = list
    .filter { it.os == OS.IOS }
    .map(SiteVisit::duration)
    .average()

println(averageIOSDuration)
// 22.0

Suppose that the ANDROID data needs to be analyzed. In order to avoid duplication, a parameter can be abstracted.
Remove duplicate code in a common way:

// Extract duplicate code into functions
fun List<SiteVisit>.averageDurationFor(os: OS) =
    filter { it.os == os }.map(SiteVisit::duration).average()

println(list.averageDurationFor(OS.ANDROID))
// 16.3

Use a higher-order function to remove duplicate code:

fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
    filter(predicate).map(SiteVisit::duration).average()

println(list.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) })
// 19.15

2. Inline function: eliminate the runtime overhead caused by lambda

Lambda expressions are normally compiled into anonymous classes. This means that every time a lambda expression is called, an extra class will be created, which will cause additional runtime overhead.

Is it possible for the compiler to generate code as efficient as java statements and extract duplicate logic into library functions? kotlin's compiler can do it. If the inline modifier is used to mark a function, the compiler will not generate the code of function call when the function is used, but replace every function call with the real code implemented by the function.

2.1 how inline functions work

When a function is declared as inline, its function body is inline, and the function will be directly replaced to the place where the function is called instead of being called normally.

// Define an inline function
inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try {
        return action()
    } finally {
        lock.unlock()
    }
}

val lock = ReentrantLock()
synchronized(lock) {
    // ...
}

Because the synchronized function has been declared as inline, each call to it generates the same code as the java synchronized statement.

fun foo(lock: Lock) {
    println("Before sync")

    synchronized(lock) {
        println("Action")
    }

    println("After sync")
}

The compiled foo function of this Code:

fun foo(lock: Lock) {
    println("Before sync")

    // synchronized function code to be inlined
    lock.lock()
    try {
        println("Action") // The body code of the lambda being inlined
    } finally {
        lock.unlock()
    }

    println("After sync")
}

Bytecode generated by lambda becomes part of the function caller's definition rather than being contained in an anonymous class that implements the function interface.

When an inline function is called, variables of function type can also be passed as parameters:

class LockOwner(
    val lock: Lock
) {
    fun runUnderLock(body: () -> Unit) {
        // Pass a variable of function type as an argument instead of a lambda
        synchronized(lock, body)
    }
}

In this case, the lambda code is not available at the point where the inline function is called, so it is not inlined. Lambda will only be called normally if the synchronized function body is inlined.

The runUnderLock function is compiled as bytecode similar to the following functions:

class LockOwner(
    val lock: Lock
) {
    // This function is similar to the bytecode that the real runUnderLock is programmed into
    fun runUnderLock(body: () -> Unit) {
         lock.lock()
         try {
             // body is not inlined because there is no lambda at the place of the call
             body()
         } finally {
             lock.unlock()
         }     
    }
}

2.2 inline collection operations

Next let's look at the functional performance of the kotlin standard library operation set.

For example: use lambda to filter a collection

data class Person(
    val name: String,
    val age: Int
)

val persons = listOf(Person("Alice", 23), Person("Bob", 43))
println(persons.filter { it.age < 30 })

// [Person(name=Alice, age=23)]

In kotlin, the filter function is declared as an inline function. It means that the filter function and the bytecode of the lambda passed to it will be inlined to the place where the filter is called. Kotlin's support for inline functions eliminates the need to worry about performance.

2.3 deciding when to declare a function inline

Using the inline keyword can only improve the performance of functions with lambda parameters, other cases need additional research.

Inlining functions with lambda parameters avoids run-time overhead. In fact, it not only saves the cost of function calls, but also saves the cost of creating anonymous classes for lambda and creating lambda instance objects.

When using the inline keyword, pay attention to the length of the code. If the inline function is very large, copying its bytecode to each call point will greatly increase the bytecode length. The code unrelated to the lambda parameter should be extracted into an independent non inline function.

2.4 using inline lambda to manage resources

A common pattern that lambda can remove duplicate code is resource management: first get a resource, complete an operation, and then release the resource. Resources can be a file, a lock, a database transaction, etc. The standard practice for patterns is to use try/finally statements.

As the synchronized function implemented earlier. But the kotlin standard library defines another function called withLock, which is an extension of the Lock interface.

val lock : Lock = ReentrantLock()
// Perform the specified operation in case of locking
lock.withLock { // ... }

// This is the definition of withLock function in Kotlin Library:
// Code to be locked is extracted into a separate method
public inline fun <T> Lock.withLock(action: () -> T): T {
    lock()
    try {
        return action()
    } finally {
        unlock()
    }
}

Use try with resource statement in java:

static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

You can use the use function in the kotlin standard library:

fun readFirstLineFromFile(path: String): String {
    // Build BufferedReader, call use function, pass a lambda to execute file operation
    BufferedReader(FileReader(path)).use { br ->
        return br.readLine()
    }
}

The use function is an extension function that is used to operate on a resource that can be closed. It takes a lambda as a parameter. The use function is also inline and does not incur any performance overhead.

3. Control flow in higher order function

3.1 return statement in lambda: return from a closed function

To compare two different methods of traversing a set:

data class Person(
    val name: String,
    val age: Int
)

val persons = listOf(Person("Alice", 23), Person("Bob", 43))
lookForAlice(persons)

fun lookForAlice(persons: List<Person>) {
    for (person in persons) {
        if (person.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not Found!")
}
// Found!

Is it safe to rewrite this code using forEach iteration? Using forEach is also safe.

fun lookForAlice(persons: List<Person>) {
    persons.forEach {
        if (it.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not Found!")
}

// Found!

If you use the return keyword in a lambda, it will return from the function that calls the lambda, not just from the lambda. Such a return statement is called a nonlocal return because it is returned from a larger block of code than the one containing the return.

Only when the function with lambda as the parameter is inline can it be returned from the outer function. The function body of forEach is inline with the function body of lambda, so it is easy to return from the function containing it when compiling. Using a return expression in a lambda that is not an inline function is not allowed.

3.2 return from lambda: return with label

You can also use local returns in lambda expressions. The local return in lambda is similar to the break expression in the for loop. It terminates the execution of the lambda and then executes from the code that calls the lambda.

To distinguish between local return and non local return, a label is used. If you want to return from a lambda expression, mark it, and then reference the label after the return keyword.

// Local return with a label
fun lookForAlice(persons: List<Person>) {
    // Label lambda expressions
    persons.forEach label@{
        if (it.name == "Alice") {
            // return@label refers to this label
            return@label
        }
    }
    println("Alice is not Found!")
}

To mark a lambda expression, place a label name (which can be any identifier) before the curly braces of the lambda, followed by an @ symbol. To return from a lambda, follow the return keyword with an @ sign, followed by the tag name.

The function name of a function with lambda as an argument can be used as a label:

// Use function name as return tag
fun lookForAlice(persons: List<Person>) {
    persons.forEach {
        if (it.name == "Alice") {
            // return@forEach from lambda expression
            return@forEach
        }
    }
    println("Alice is not Found!")
}

3.3 anonymous function: local return is used by default

// Using return in anonymous functions
fun lookForAlice(persons: List<Person>) {
    // Using anonymous functions instead of lambda expressions
    persons.forEach(fun(person) {
        // return points to the nearest function: an anonymous function
        if (person.name == "Alice") return
        println("${person.name} is not Alice!")
    })
}

// Bob is not Alice!

Anonymous functions omit function names and parameter types.

// Using anonymous functions in filter
persons.filter(fun(person): Boolean {
    return person.age < 30
})

If you use the body of an expression function, you can omit the return value type

// Use expression function body anonymous function
persons.filter(fun(person) = person.age < 30)

In anonymous functions, return expressions without labels are returned from anonymous functions, not from functions that contain anonymous functions. Return from the most recent function declared with the fun keyword.

Lambda expressions do not use the fun keyword, so return in lambda is returned from the outermost function. Anonymous functions use the fun keyword, which is returned from the recently declared function of fun.

If my article is helpful to you, please give me a compliment

Posted by renegade44 on Sat, 07 Dec 2019 10:40:46 -0800