Write at the beginning: I plan to write a Kotlin series of tutorials, one is to make my own memory and understanding more profound, two is to share with the students who also want to learn Kotlin. The knowledge points in this series of articles will be written in order from the book “Kotlin In Action”. While displaying the knowledge points in the book, I will also add corresponding Java code for comparative learning and better understanding.

Kotlin Tutorial (1) Basic Kotlin Tutorial (2) Functions Kotlin Tutorial (3) Classes, Objects and Interfaces Kotlin Tutorial (4) Nullability Kotlin Tutorial (5) Types Kotlin Tutorial (6) Lambda programming Kotlin Tutorial (7) Operator overloading and Other conventions Higher-order functions Kotlin tutorial (9) Generics


Declare higher-order functions

A higher-order function is one that takes another function as an argument or return value. In Kotlin, functions can be represented as lambda or function references. Therefore, any function that takes a lambda or function reference as an argument, or that returns a lambda or function reference, is a higher-order function. For example, the library filter function takes a judgment as an argument:

list.filter { x > 0 }
Copy the code

Function types

To declare a function that takes lambda as an argument, you need to know how to declare the type of the corresponding parameter. Before we do that, let’s look at a simple example of storing lambda expressions in local variables. We’ve actually seen how to do this without declaring a type, depending on Kotlin’s type derivation:

val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }
Copy the code

The compiler deduces that the sum and action variables have function types. Now let’s see what the display type declarations for these variables look like:

val sum: (Int, Int) -> Int = { x, y -> x + y }
val action: () -> Unit = { println(42) }
Copy the code

To declare a function type, enclose the function parameter type in parentheses, followed by an arrow and the function return type.

You’ll recall that the Unit type is used to indicate that a function does not return any useful value. When declaring a normal function, the return value of type Unit can be omitted, but a function type declaration always requires an explicit return type, so Unit cannot be omitted.

The types of parameters are omitted from the lambda expression {x, y -> x + y} because their types are specified in the variable declaration section of the function type and do not need to be repeated in the definition of the lambda itself.

Just like any other method, the return value of a function type can also be marked as nullable:

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

It is also possible to define a nullable variable of a function type. To make it clear that the variable itself is nullable, and not the return type of a function type, you need to enclose the entire function type definition in parentheses and add a question mark after the parentheses:

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

Notice the subtle difference between these two examples. If you omit the parentheses, you declare a nullable function type instead of a nullable function type variable.

The parameter name of the function type

You can specify names for parameters in a function type declaration:

Fun performRequest(url: String, callback: (code: Int, content: String) -> Unit */ } >>> val url ="http://kotl.in">>> performRequest(url) {code, content -> /*... * /} / / you can use the define name > > > performRequest (url) {code page - > / *... */} // You can also change the parameter nameCopy the code

Parameter names do not affect type matching. When you declare a lambda, you don’t have to use the exact same parameter names as in the function type declaration, but naming improves code readability and can be used for IDE code completion.

Call the function as an argument

Now that we know how to declare a higher-order function, we will discuss how to implement it. The first example will be as simple as possible and use the same declaration as the previous lambda sum. This function performs any operation on the two numbers 2 and 3, and then prints the result.

fun twoAndThree(operation: (Int, Int) -> Int) {
    val result = operation(2, 3)
    println("The result is $result")
}

>>> twoAndThree { a, b -> a + b }
The result is 5
>>> twoAndThree { a, b -> a * b }
The result is 6
Copy the code

The syntax for calling a function as an argument is the same as for calling a normal function: place parentheses after the function name and the arguments within parentheses. For a more interesting example, let’s implement the most commonly used library function: the filter function. To keep things simple, we’ll implement a filter function based on String, but in much the same way as the generic version of geometry:

fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if (predicate(element)) sb.append(element)
    }
    return sb.toString()
}
Copy the code

The filter function takes a Boolean as an argument. The filter type is a function that takes a character as an argument and returns a Boolean value. The judgment needs to return true if the character passed to it is to appear in the final returned string, and false if not. The implementation of the filter function is straightforward. It checks each character to see if it fits the satisfaction. if so, it adds the character to the StringBuilder containing the result.

Use function classes in Java

The principle behind this is that function types are declared as ordinary interfaces, and variables of a function type are an implementation of the FunctionN interface. The Kotlin library defines a series of interfaces for functions with different numbers of arguments: Function0

functions with no arguments, Function1 functions with one argument, and so on. Each interface defines an Invoke method, which executes the function. A variable of function type is an instance of the implementation class that implements the corresponding FunctionN interface, and the invoke method that implements the class contains the body of a lambda function. Kotlin can be easily called in Java using the function type. Java 8 lambdas are automatically converted to values of function type. ,r>

/*Kotlin definition */ fun processTheAnswer(f: (Int) -> Int) { println(f(42)) } /*Java*/ >>> processTheAnswer(number -> number + 1) 43Copy the code

In older versions of Java, you could pass an instance of an anonymous class that implemented the Invoke method in a function interface:

processTheAnswer(new Function1<Integer, Integer>() {
            @Override
            public Integer invoke(Integer integer) {
                System.out.println(integer);
                return integer+ 1; }});Copy the code

Extension functions from the Kotlin standard library that take lambda as an argument can easily be used in Java. But note that they don’t look as intuitive as Kotlin’s name — you must explicitly pass a receiver object as the first argument:

    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        strings.add("42");
        CollectionsKt.forEach(strings, new Function1<String, Unit>() {
            @Override
            public Unit invoke(String s) {
                System.out.println(s);
                returnUnit.INSTANCE; }}); } // output 42Copy the code

In Java, a function or lambda can return Unit. But because the Unit type has a value in Kotlin, you need to return it explicitly.

Function type parameter default value and NULL

When declaring parameters of a function type, you can specify default values for the parameters. To see how defaults can be used, let’s go back to the joinToString function in Tutorial 2. Here is its final implementation:

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)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
Copy the code

This implementation is flexible, but it doesn’t let you control the key point of the transformation: how elements in the collection are converted to strings. Stringbuilder.append (o: Any?) , which always converts an object to a string using the toString method. In most cases this will do, but not always. To solve this problem, you can define a parameter of function type and use a lambda as its default value.

fun <T> Collection<T>.joinToString(
        separator: String = ",",
        prefix: String = "",
        postfix: String = "", transform: (T) -> String = {it.toString()} // default implementation): String {val result = prefix (String)for ((index, element) in this.withIndex()) {
        ifAppend (separator) result.append(transform(element)) // Use function argument to convert} result.append(postfix)return result.toString()
}

fun main(args: Array<String>) {
    val letters = listOf("Alpha"."Beta")
    println(letters.joinToString())
    println(letters.joinToString { it.toLowerCase() })
    println(letters.joinToString(separator = ! "" ", postfix = ! "" ", transform = {it.toupperCase ()}))} Alpha,Beta Alpha,Beta Alpha! BETA!Copy the code

This is a generic function: it takes a type parameter T to indicate the type of the element in the collection. The transform will receive arguments of this type. No special syntax is required to declare a default value for a function type — just place lambda after the = sign as a value. The above example shows different ways of calling a function: omitting the entire lambda (using the default toString for conversion), passing the lambda outside parentheses, or passing it as a named argument. In addition to the default implementation for selective passing, another option is to declare a nullable function type as an argument. Note that the function passed in as an argument cannot be called directly.

fun foo(callback: (() -> Unit)?) {if(callback ! = null) { callback() } }Copy the code

If you don’t want to nullify, use the function type as a concrete implementation of an interface that contains the Invoke method. As a normal method, invoke can safely invoke the syntax: callback? The invoke ().

Function that returns a function

Returning another function from a function is not as common as passing the function as an argument, but it is still very useful. Imagine that a piece of logic in a program might change depending on the state of the program or some other condition — for example, the calculation of shipping costs depends on the mode of transportation chosen. You can define a function that selects the appropriate logical variant and returns it to another function.

enum class Delivery {STANDARD, EXPEDITED }

class Order(val itemCount: Int)

fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double {
    if (delivery == Delivery.EXPEDITED) {
        return{order -> 6 + 2.1 * order. ItemCount}}return{the order - > 1.2 * order. ItemCount}} > > > val calculator = getShippingCostCalculator (Delivery. EXPEDITED) > > > println ("Shipping costs ${calculator(Order(3))}")
Shipping costs 12.3
Copy the code

To declare a function that returns another function, you need to specify a function type as the return type. GetShippingCostCalculator return a function, the function with the Order as a parameter and return a value of type Double. To return a function, you write a return expression that follows a lambda, a member reference, or another expression of a function type, such as a local variable of a function type.

Lambda removes duplicate code

Together with lambda expressions, function types form a great tool for creating reusable code. Let’s look at an example of analyzing web site visits. The SiteView class is used to save the path of each visit. Duration and user’s operating system. Different operating systems use enumeration types to represent:

enum class OS {WINDOWS, LINUX, MAC, IOS, ANDROID }

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

val log = listOf(SiteVisit("/", 34.0, OS. WINDOWS), SiteVisit ("/", 22.0, OS. The MAC), SiteVisit ("/login", 12.0, OS. WINDOWS), SiteVisit ("/signup", 8.0, OS. IOS), SiteVisit ("/", 16.3, OS. ANDROID))Copy the code

Imagine if you needed to display the average access time from a Windows machine, using the Average function to do this:

val averageWindowsDuration = log.filter { it.os == OS.WINDOWS } .map(SiteVisit::duration) .average() >>> Println (averageWindowsDuration) 23.0Copy the code

Now suppose you want to calculate the same data from Mac users. To avoid duplication, you can abstract the platform type as a parameter.

fun List<SiteVisit>.averageDurationFor(os: OS) = filter {it.os == OS}.map {it.duration}.average() >>> println(log.averagedurationfor (os.windows)) 23.0 >>> Println (log. AverageDurationFor (OS. The MAC)) 22.0Copy the code

Using this function as an extension function increases readability. You can even declare this function as a local extension function if it is only useful in a local context. But that’s not enough. Imagine if you were interested in the average time to access from a mobile platform.

val averageMoblieDuration =
            log.filter { it.os in setOf(os.ios, os.android)}.map(SiteVisit::duration).average() >>> println(averageMoblieDuration) 12.15Copy the code

It is no longer possible to represent different platforms with a single parameter. You may also need to use more complex conditional query logs. For example, what is the average time it takes to access a registration page from IOS? Lambda can help by abstracting the required condition into a parameter using function types.

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

>>> println(log.averageDurationFor {it.os in setOf(os.android, os.ios)}) 12.15 >>> println(log.averagedurationfor {it.os == os.ios && it.path =="/signup"})
8.0
Copy the code

Function types help eliminate duplicate code. If you’re tempted to copy and paste a piece of code, chances are that duplication could have been avoided. Using lambda, you can extract not only repeated data, but also repeated behavior.

Some well-known design patterns can be simplified by function types and lambda expressions. Take the policy pattern. In the absence of lambda expressions, you need to declare an interface and provide implementation classes for none of the possible policies. Using function types, you can describe policies with a common function type and then pass different lambda expressions as different policies.

Inline functions: Eliminate runtime overhead associated with lambda

Lambda expressions are compiled into anonymous classes as normal. This means that without calling a lambda expression once, an additional class will be created. And if a lambda captures a variable, a new object will be created each time it is called. This imposes additional runtime overhead, making lambda less efficient than using a function that directly executes the same code. Is it possible for the compiler to generate code as efficient as Java statements, but still be able to extract repetitive logic into library functions? Yes, Kotlin’s compiler can. If a function is marked with an inline modifier, the compiler does not generate the code for the function call when the function is in use, but replaces each function call with the actual code for the function implementation.

How do inline functions work

When a function is declared inline, its function body is inline — in other words, the function is replaced directly where the function was called, rather than being called normally. Take a look at an example to understand the resulting code.

inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try {
        returnaction() } finally { lock.unlock() } } val l = Lock() synchronized(l) {... }Copy the code

This function is used to ensure that a shared resource is not concurrently accessed by multiple threads. The function locks a Lock object, executes a code block, and releases the Lock. Calling this function is exactly the same syntax as using synchronized statements in Java. The difference is that Java’s synchronized statements can be used on any object, whereas this function requires passing in an instance of Lock. The definition shown here is just an example; the Kotlin library defines a version of synchronized functions that can accept any object as an argument.

Because the synchronized function has been declared inline, the code generated each time it is called is the same as Java’s synchronized statement. Consider the following example of synchronized() :

fun foo(l: Lock) {
    println("Before sync")
    synchronized(l) {
        println("Action")
    }
    println("After sync")}Copy the code

The following code will compile to the same bytecode:

fun __foo__(l: Lock) {
   println("Before sync")
   l.lock()
    try {
        println("Action")
    } finally {
        l.unlock()
    }
    println("After sync")}Copy the code

Lambda expressions and implementations of synchronized functions are inlined. The 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. Note that we can also pass variables of function type as arguments when calling inline functions:

class LockOwner(val lock: Lock) {
    fun runUnderLock(body: () -> Unit) {
        synchronized(lock, body)
    }
}
Copy the code

In this case, the code for lambda is not available at the point at which the inline function is called and therefore will not be inlined. Lambda will only be called properly if synchronized’s function body is inlined. The runUnderLock function is compiled into bytecode similar to the following function:

class LockOwner(val lock: Lock) { fun __runUnderLock__(body: () -> Unit) {lock.lock() try {body() //body is not inline because there is no lambda} finally {lock.unlock()}}}Copy the code

If two different locations use the same inline function, but with different lambdas, then the inline function is inlined separately at each called location. The code for the inline function is copied to two different places where it is used and replaced with different lambdas.

Limitations on inline functions

Given the way inlining works, not all functions that use lambda can be inlined. When functions are inlined, the functions of the lambda expressions as arguments are replaced directly into the resulting code. This limits the use of the corresponding lambda arguments in the function body. Such code can be easily inlined if the lambda argument is called. But if lambda arguments are stored somewhere so that they can be used later, the code for the lambda expression cannot be inlined, because an object containing the code must exist. Generally, a parameter can be inline if it is called directly or passed as a parameter to another inline function. Otherwise, the compiler disallows the parameter to be inlined and gives the error message “Illegal usage of inline-parameter”. For example, many functions that operate on sequences return instances of classes that represent the corresponding sequence operation and accept a lambda as an argument to the constructor. Here is the definition of the sequence. map function:

public fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
    return TransformingSequence(this, transform)
}
Copy the code

The map function does not directly call the function passed in as the transform parameter. Instead, you pass the function to a class constructor, which stores it in a property. To support this, the lambda passed as the transform argument needs to be compiled into a standard non-inline representation, that is, an anonymous class that implements the function interface. If a function expects two or more lambda arguments, you can choose to inline only some of them. This makes sense because a lambda may contain a lot of code or be used in a way that does not allow inline. To receive arguments that are not inline lambda, we can mark them with the noline modifier:

inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {}
Copy the code

The compiler fully supports inlining cross-module functions or functions defined by third-party libraries. It is also possible to call most inline functions in Java, but these calls are not inlined but compiled as normal function calls.

Inline collection operation

Let’s take a closer look at the performance of the Kotlin library operation set functions. Most library collection functions take lambda arguments. Isn’t it more efficient to implement these operations directly than using library functions? For example, let’s compare the way a list of people is filtered in the following two codes:

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

val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.filter{ it.age <= 30 })
[Person(name=Hubert, age=26)]
Copy the code

The previous code can be implemented without lambda expressions:

val result = mutableListOf<Person>()
for (person in people) {
    if (person.age <= 30) result.add(person)
}
println(result)
Copy the code

In Kotlin, the filter function is declared inline. This means that the filter function, along with the bytecode passed to its lambda, is inline where filter is called. Ultimately, the bytecode produced by the first implementation is roughly the same as the bytecode produced by the second. You can safely use language-friendly collection operations, and Kotlin’s support for inline functions lets you avoid performance concerns. Imagine now that you call the filter and map operations:

>>> println(people.filter { it.age > 30 }.map(Person::name))
[Bob]
Copy the code

This example uses a lambda expression and a member reference. Again, the filter and map functions are declared inline, so their functions are inlined, so no additional classes or objects are generated. But the above code creates an intermediate collection to hold the results of the list filtering, which the code generated by the filter function adds elements to and the code generated by the map function reads. If you have a large number of collection elements to deal with, and the overhead of running intermediate collections becomes a problem, you can add an asSquence call to the chain of calls, replacing collections with sequences. But as you saw earlier, the lambda used to process the sequence is not inline. Each intermediate sequence is represented as an object holding the lambda in its field, and the end operation causes a chain of calls made up of each intermediate sequence call to be executed. Therefore, even if operations on a sequence are lazy, you should not always try to add asSquence to the call chain of collection operations. This is only useful when dealing with collections of large amounts of data, knowing that collections can be handled using ordinary collection operations.

Determines when a function is declared inline

Inline reduces the run-time overhead of functions (including the creation of anonymous classes), but it is based on copying labeled functions to each call point, thus increasing the size of the bytecode if the function body is too much code. Consider that the JVM itself already provides powerful inline support: it analyzes the execution of code and inlines function calls whenever it could benefit from inlining. Another point is that Kotlin’s inline functions do not have the same effect when called by Java. Finally, we should be cautious about adding inline, which only marks smaller functions that need to be embedded in the caller.

Control flow in higher-order functions

When you start using lambdas to replace imperative code constructs like loops, you quickly find yourself running into problems with return expressions. Putting a return statement in the middle of the loop is a simple matter. But what if we converted the loop to a function like filter? How does return work in this case?

Return statement in lambda: Returns from a closed function

To compare two different ways of traversing a set. In the following code, it’s clear that if a name is Alice, it should return from lookForAlice:

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    lookForAlice(people)
}

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

fun lookForAlice(people: List<Person>) {
    for (person in people) {
        if (person.name == "Alice") {
            println("Found!")
            return
        }
    }
    println("Alice is not found"} // Output Found!Copy the code

Is it safe to rewrite this code using forEach iterations? Would the return statement behave the same? Yes, as the following code shows, forEach is secure.

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

If you use the return keyword in a lambda, it will return from the function calling the lambda, not just from the lambda. Such a return statement is called a nonlocal return because it returns from a larger code block than the one containing the return. To understand the logic behind this rule, think of Java functions that use the return keyword in a for loop or synchronized block of code. Clearly returns from functions, rather than loops or blocks of code, and Kotlin retains the same behavior when using functions that take lambda. Note that the return from the outer function is only possible if the function taking lambda is an inline function. In the above example forEach’s function body is inlined with the body of the lambda function, so it is easy to return from the containing function at compile time. Using a return expression in a lambda of a non-convergent function is not allowed.

Return from lambda: Returns using the label

You can also use local returns in lambda expressions. A local return in a lambda is similar to a break expression in a for loop. It terminates the execution of the lambda and then executes from the lambda’s code. To distinguish between layout returns and non-local returns, use tags. To return from a lambda expression you can mark it and then reference the tag after the return keyword.

Fun lookForAlice(people: List<Person>) {people.foreach label@{// Declare the labelif (it.name == "Alice") {
            return}} println("Alice might be somewhere")
}

>>> lookForAlice(people)
Alice might be somewhere
Copy the code

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 the lambda, place an @ symbol after the return keyword, followed by the tag name. Or the function name of a function that defaults to lambda as an argument:

fun lookForAlice(people: List<Person>) {
    people.forEach {
        if (it.name == "Alice") {
            return@forEach
        }
    }
    println("Alice might be somewhere")}Copy the code

If you explicitly specify the label of the lambda expression, using the function name as the label has no effect. A lambda expression cannot have more than one tag.

Tagged this expression

The same rule applies to the tag of this expression. A lambda with a receiver that contains an implicit context object can be accessed via this reference. If you label a lambda with a receiver, its implicit receiver can be accessed through the corresponding tagged this expression.

    println(StringBuilder().apply sb@ {
        listOf(1, 2, 3).apply {
            [email protected](this.toString())
        }
    })
Copy the code

As with tags used in return expressions, labels for lambda expressions can be explicitly specified, or function names can be used directly as labels.

Anonymous functions: Local returns are used by default

Anonymous functions are a different way of writing blocks of code that are passed to functions. Let’s start with an example:

Fun lookForAlice(people: List<Person>) {people.foreach (fun(Person) {// Use anonymous functions instead of lambdaif (person.name == "Alice") {
                    return
                }
                println("${person.name} is not Alice")
            }
    )
}
>>> lookForAlice(people)
Bob is not Alice
Copy the code

Anonymous functions look like normal functions, except that their names and argument types are omitted. Here’s another example:

people.filter(fun(person): Boolean {
        return person.age < 30
    })
Copy the code

Anonymous functions and normal functions have the same rules for specifying return value types. Code block anonymous functions require an explicit return type, which can be omitted if the expression function body is used.

people.filter(fun(person): Boolean = person.age < 30)
Copy the code

In anonymous functions, an unlabeled return expression is returned from the anonymous function, not from the function that contains the anonymous function. The rule is simple: return returns from the function recently declared with the fun keyword. Lambda expressions do not use the fun keyword, so the return in lambda returns from the outermost function. The anonymous function uses FUN, so in the previous example the anonymous function is the closest conforming function. So a return expression returns from an anonymous function, not from the outermost function.

Note that although anonymous functions look like regular functions, they are just another syntactic form of a lambda expression. The same applies to anonymous functions about how lambda expressions are implemented and how inline functions are inlined.