Kotlin studies (8) : Higher-order functions and Lambda expressions

Kotlin functions are first-class, which means they can be stored in variables and data structures, passed as arguments to and returned from other higher-order functions. Functions can be manipulated just like any other non-function value.

To facilitate this, Kotlin, a statically typed programming language, uses a series of function classes to represent functions and provides a specific set of language constructs, such as lambda expressions.

Higher-order functions

Higher-order functions are functions that use functions as arguments or return values.

A good example of a higher-order function is the functional style fold of a collection, which takes an initial cumulative value with an coalesce function and builds the return value by concatenating the current cumulative value with each collection element into the cumulative value:

fun <T, R> Collection<T>.fold(
    initial: R, 
    combine: (acc: R.nextElement: T) - >R
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}
Copy the code

In the code above, the parameter Combine has function type (R, T) -> R, so the fold takes as a parameter a function that takes two parameters of type R and T and returns a value of type R. This function is called inside the for loop and its return value is assigned to Accumulator.

To call the fold, you need to pass it an instance of a function type as an argument, and lambda expressions are widely used for this purpose in higher-order function calls.

fun main(a) {
    //sampleStart
    val items = listOf(1.2.3.4.5)

    // Lambdas are blocks of code enclosed in curly braces.
    items.fold(0, { 
        // If a lambda expression has arguments, the argument is preceded by "->"
        acc: Int, i: Int -> 
        print("acc = $acc, i = $i,") 
        val result = acc + i
        println("result = $result")
        // The last expression in a lambda expression is the return value:
        result
    })

    // The argument type of a lambda expression is optional, if it can be inferred:
    val joinedToString = items.fold("Elements:", { acc, i -> acc + "" + i })

    // Function references can also be used for higher-order function calls:
    val product = items.fold(1.Int::times)
    //sampleEnd
    println("joinedToString = $joinedToString")
    println("product = $product")}Copy the code

Function types

Kotlin uses a function type like (Int) -> String to handle function declarations: val onClick: () -> Unit =… .

These types have special representations that correspond to function signatures — their arguments and return values:

  • All function types have a list of parameter types enclosed in parentheses and a return type:(A, B) -> CIndicates that the acceptance types are respectivelyABTwo arguments and return oneCThe function type of a type value. The parameter type list can be empty, for example() -> A.UnitThe return type cannot be omitted.
  • The function type can have an additionalThe receiverType, which is specified before the point in the representation: typeA.(B) -> CCan be used inAOn the receiver objectBType argument to call and return oneCA function of type value. Functional literals with receivers are usually used with these types.
  • Suspended functions are a special class of function types and have one representationsuspendModifier, for examplesuspend () -> Unitorsuspend A.(B) -> C.

Function type notation optionally includes function parameter names: (x: Int, y: Int) -> Point. These names can be used to indicate the meaning of the parameters.

To specify a nullable function type, use parentheses as follows: ((Int, Int) -> Int)? .

Function types can also be concatenated with parentheses :(Int) -> ((Int) -> Unit.

Arrow notation is right associative, (Int) -> (Int) -> Unit is equivalent to the previous example, but not equal to (Int) -> (Int)) -> Unit.

You can also give a function type another name by using a type alias:

typealias ClickHandler = (Button, ClickEvent) -> Unit
Copy the code

Function type instantiation

There are several ways to get an instance of a function type:

  • A code block that uses a function numeric value, in one of the following forms:

    • Lambda expressions:{ a, b -> a + b }.
    • Anonymous functions:fun(s: String): Int { return s.toIntOrNull() ?: 0 }

    Function literals with receivers can be used as values of function types with receivers.

  • Callable references with existing declarations:

    • Top-level, local, member, extension function, :::isOdd,String::toInt.
    • Top-level, member, extended properties:List<Int>::size.
    • Constructor:::Regex

    This includes callable references to bindings that point to specific instance members: foo::toString.

  • Example of a custom class that implements a function-type interface:

class IntTransformer: (Int) - >Int {
    override operator fun invoke(x: Int): Int = TODO()
}

val intFunction: (Int) - >Int = IntTransformer()
Copy the code

With sufficient information, the compiler can infer the function type of the variable:

val a = { i: Int -> i + 1 } // The inferred type is (Int) -> Int
Copy the code

Function types with and without a receiver are interchangeable with non-literals, where the receiver can substitute for the first argument, and vice versa. For example, values of type (A, B) -> C can be passed or assigned to places where values of type A.(B) -> C are expected, and vice versa:

fun main(a) {
    //sampleStart
    val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
    val twoParameters: (String, Int) -> String = repeatFun // OK

    fun runTransformation(f: (String.Int) - >String): String {
        return f("hello".3)}val result = runTransformation(repeatFun) // OK
    //sampleEnd
    println("result = $result")}Copy the code

By default, function types without receivers are inferred, even if variables are initialized by extending function references. If you want to change this, specify the variable type explicitly.

Function type instance call

A value of a function type can be invoked (…) The operator calls f.invoke(x) or f(x) directly.

If the value has a receiver type, the receiver object should be passed as the first argument. Another way to call a function type value with a receiver is to prefix it with the receiver object, as if the value were an extension function: 1.foo(2).

Such as:

fun main(a) {
    //sampleStart
    val stringPlus: (String, String) -> String = String::plus
    val intPlus: Int. (Int) - >Int = Int::plus

    println(stringPlus.invoke("< -"."- >"))
    println(stringPlus("Hello, "."world!"))

    println(intPlus.invoke(1.1))
    println(intPlus(1.2))
    println(2.intPlus(3)) // Class extension call
    //sampleEnd
}
Copy the code

Lambda expressions and anonymous functions

Lambda expressions and anonymous functions are functional literals, functions that are not declared but are passed immediately as expressions. Consider the following example:

max(strings, { a, b -> a.length < b.length })
Copy the code

The function Max is a higher-order function because it takes a function as a second argument. Its second argument is an expression that is itself a function, called a function numeric value, which is equivalent to the following named function:

fun compare(a: String, b: String): Boolean = a.length < b.length
Copy the code

Lambda expression syntax

The full syntax of a Lambda expression is as follows:

val sum: (Int.Int) - >Int = { x: Int, y: Int -> x + y }
Copy the code
  • Lambda expressions are always enclosed in curly braces.
  • Parameter declarations in full syntactic form are enclosed in curly braces and have optional type annotations.
  • The body of the function follows one->After.
  • If the inferred return type of the lambda is notUnit, then the last (or possibly single) expression in the lambda body is treated as the return value.

If you left all the optional annotations, they would look like this:

val sum = { x: Int, y: Int -> x + y }
Copy the code

Pass the trailing lambda expression

According to Kotlin’s convention, if the last argument to a function is a function, then the lambda expression passed as the corresponding argument can be placed outside the parentheses:

val product = items.fold(1) { acc, e -> acc * e }
Copy the code

This syntax is also known as trailing lambda expressions.

If the lambda expression is the only argument when called, the parentheses can be omitted entirely:

run { println("...")}Copy the code

it: The implicit name of a single parameter

It is common for a lambda expression to have only one argument.

If the compiler can parse the signature without any arguments, there is no need to declare arguments and -> can be omitted. This parameter is implicitly declared as it:

ints.filter { it > 0 } // This literal is of type "(it: Int) -> Boolean"
Copy the code

Returns a value from a lambda expression

You can explicitly return a value from lambda using qualified return syntax. Otherwise, the value of the last expression is implicitly returned.

Therefore, the following two fragments are equivalent:

ints.filter {
    val shouldFilter = it > 0
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0
    return@filter shouldFilter
}
Copy the code

This convention, along with passing lambda expressions outside parentheses, supports LINQ-style language-integrated Query code:

strings.filter { it.length == 5 }.sortedBy { it }.map { it.uppercase() }
Copy the code

The s underscore is used for unused variables

If lambda expression arguments are not used, then its name can be replaced with an underscore:

map.forEach { _, value -> println("$value!")}Copy the code

Destruct in a lambda expression

In lambda expressions, deconstruction is described as part of the deconstruction declaration.

Anonymous functions

One thing missing from the above lambda expression syntax is the ability to specify the return type of a function. In most cases, this is unnecessary. Because the return type can be inferred automatically. However, if you do need to specify explicitly, you can use another syntax: anonymous functions.

fun(x: Int, y: Int): Int = x + y
Copy the code

An anonymous function looks a lot like a regular function declaration, except that its name is omitted. The function body can be either an expression (as shown above) or a code block:

fun(x: Int, y: Int): Int {
    return x + y
}
Copy the code

Parameters and return types are specified in the same way as regular functions, except that parameter types that can be inferred from the context can be omitted:

ints.filter(fun(item) = item > 0)
Copy the code

The mechanism for return type inference for anonymous functions is the same as for normal functions: the return type is automatically inferred for anonymous functions with an expression body, but the return type for functions with a code block body must be explicitly specified (or assumed to be Unit).

When an anonymous function is passed as an argument, it is enclosed in parentheses. The shorthand syntax that allows functions to be left outside parentheses applies only to lambda expressions.

Another difference between Lambda expressions and anonymous functions is the behavior of non-local returns. An unlabeled return statement always returns in a function declared with the fun keyword. This means that a return in a lambda expression will return from the function that contains it, and a return in an anonymous function will return from the anonymous function itself.

closure

Lambda expressions or anonymous functions (as well as local functions) can access their closures, which contain variables declared in external scopes. Variables captured in closures can be modified in lambda expressions:

var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)
Copy the code

The function numeric value with the receiver

Function types with receivers, such as a. (B) -> C, can be instantiated with A special form of function literals — function literals with receivers.

As mentioned above, Kotlin provides the ability to call an instance of a function type with a receiver (when a receiver object is provided).

Inside such function literals, the recipient object passed to the call becomes implicit this, so that members of the recipient object can be accessed without any additional qualifiers, and the recipient object can also be accessed using the this expression.

This behavior is similar to extension functions, which also allow access to members of the receiver object within the function body.

Here is an example of a function literal with a receiver and its type, where plus is called on the receiver object:

val sum: Int. (Int) - >Int = { other -> plus(other) }
Copy the code

The anonymous function syntax allows you to specify the recipient type of the function literal directly. This is useful if you need to declare a variable using a function type with a receiver and use it later.

val sum = fun Int.(other: Int): Int = this + other
Copy the code

Lambda expressions can be used as function literals with receivers when the receiver type can be inferred from the context.

class HTML {
  fun body(a){... }}fun html(init: HTML. () - >Unit): HTML {
  val html = HTML()  // Create the receiver object
  html.init(a)// Pass the receiver object to the lambda
  return html
}

html {       // The lambda with the receiver starts here
  body()   // Calls a method of the receiver object
}
Copy the code