Kotlin core syntax: kotlin Lambda programming

Keywords: Android Lambda Java Programming

Lambda programming

1. Lambda expressions and member references

Introduction to Lambda: code blocks as function parameters

// lambda expression presentation listener
button.setOnClickListener { /* ... */ }

Lambda and sets

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

>>>     val list = listOf(Person("kerwin", 12), Person("Bob", 23))
//      Use lambda to search the collection and compare the age to find the largest element
>>>     // fun <T, R : Comparable<R>> Iterable<T>.maxBy(selector: (T) -> R): T?
//      Receive an element in a collection as an argument (use it to refer to it) and return the value used for comparison, concise writing
//      If there is only one parameter lambda, and the type of this parameter can be derived, the default parameter name it will be generated
>>>     println(list.maxBy { it.age })

//      The code snippet in curly braces is a lambda expression, which is passed to this function as an argument
//      This lambda takes a parameter of type Person and returns its age
>>>     list.maxBy { person -> person.age }



//      If a lambda happens to be a delegate of a function or property, you can replace it with a member reference
>>>     println(list.maxBy(Person::age))

Syntax of Lambda expressions

A lambda encodes a behavior, passes it around as a value, and can be declared independently and stored in a variable.

// Syntax of lambda expression
// Parameter - > function body
{ x: Int, y: Int -> x + y }

// kotlin's lambda expression is always surrounded by curly braces ({}).
// Arguments are not enclosed in parentheses. Arrows separate the argument list from the function body of the lambda

You can store lambda expressions in a variable and treat this variable as a normal function

 // Use variable to store lambda, parameter type cannot be derived, parameter type must be specified explicitly
 val sum = { x: Int, y: Int -> x + y }
 println(sum(1, 2)) // Call lambda saved in variable

In kotlin, if the lambda expression is the last argument of a function call, it can be placed outside the bracket

 list.maxBy() { person: Person ->
        person.age
    }

// When lambda is the only argument of a function, empty parentheses in the calling code can be removed
// Write out parameter types explicitly
list.maxBy { person: Person ->
        person.age
    }

// The parameter type can not be written, and will be deduced according to the context
list.maxBy { person -> person.age }

Use named arguments to pass lambda

    val list = listOf(Person("kerwin", 12), Person("Bob", 23))
    // Passing lambda as a named argument
    val names = list.joinToString(separator = " ", transform = { person: Person ->
        person.name
    })
    println(names) // kerwin Bob


    // You can send lambda out of parentheses
    val names = list.joinToString(separator = " ") { person: Person ->
        person.name
    }

Accessing variables in scope

You can use lambda inside a function, access its parameters, and define local variables before lambda

// Using function parameters in lambda
fun printMessageWithPrefix(messages: Collection<String>, prefix: String) {
    messages.forEach {
        // Accessing the prefix parameter in lambda
        println("$prefix $it")
    }
}

val messages = listOf("404", "403")
printMessageWithPrefix(messages, "error: ")

kotlin allows the lambda to access non final variables internally or even modify them, and access external variables from within the lambda, which is said to be captured by the lambda.

Member reference

// Member reference syntax, using:: operator
// Class: Members
Person::age

: operator can convert a function to a value, such as: val getAge = Person::age, which is called member reference

You can also reference top-level functions (not members of a class)

fun test() = println("test")

// Reference to the top-level function, omitting the name of the class, directly starting with::
// Member reference:: test is passed to the library function as an argument, which calls the corresponding function
run(::test)

If a lambda is to delegate to a function that takes multiple parameters, it is very convenient to replace it with a member reference

    // This lambda is delegated to the sendEmail function
    val action = { person: Person, message: String ->
        sendEmail(person, message)
    }

    // Member references can be used instead
    val nextAction= ::sendEmail

 >>> action(Person("kerwin",12), "Having dinner")
 >>> nextAction(Person("bob",34), "Having dinner")

You can use constructor references to store or defer the action of creating class instances. A constructor reference takes the form of specifying a class name after a double colon (::)

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

    // Construct method reference, the action of creating Person instance is saved as value
    val createPerson = ::Person
    val person = createPerson("kerwin", 23)
    println(person)

Extension functions can also be referenced

// Extension function of Person class
fun Person.isAdult() = this.age >= 21

val person = Person("kerwin", 22)
// Although isAdult is not a member of the Person class, it can also be accessed by reference
val predicate = Person::isAdult
println(predicate(person))

2. Functional API of set

Foundation: filter and map

Bottom source code of filter function:

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

public inline fun <T, C : MutableCollection<in T>> Iterable<T>.filterTo(destination: C, predicate: (T) -> Boolean): C {
    for (element in this) if (predicate(element)) destination.add(element)
    return destination
}

From the source code of the filter function, it can be seen that the filter function traverses the set and selects the elements that meet the given lambda and return true. These elements that meet the conditions are stored in the new set.

val list = listOf(1, 2, 3, 4)
// Filter out even elements
println(list.filter { it % 2 == 0 }) // [2, 4]

val list = listOf(Person("kerwin", 23), Person("Bob", 12))
// Filter out people over 18
println(list.filter { it.age >= 18 }) // [Person(name=kerwin, age=23)]

map function source code:

public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}

public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.mapTo(destination: C, transform: (T) -> R): C {
    for (item in this)
        destination.add(transform(item))
    return destination
}

It can be seen from the source code of map function that map function applies the given lambda to each element in the collection and stores the result in a new collection. The number of elements in the new set remains the same, but each element is transformed according to the given lambda.

val list = listOf(1, 2, 3, 4)
println(list.map { it * it }) // [1, 4, 9, 16]

val list = listOf(Person("kerwin", 23), Person("Bob", 12))
// Just a list of names, you can use map transformation
println(list.map { it.name }) // kerwin, Bob]
// You can use member references
list.map(Person::name)

// filter and map can be combined
// Output names older than 18
println(list.filter { it.age >= 18 }.map { it.name }) // [kerwin]

// Find out the oldest person: first find the oldest person in the set, and then filter the oldest person
val maxAge = list.maxBy(Person::age)?.age
println(list.filter { it.age == maxAge })

You can also apply filtering and transformation functions to Map sets:

val map = mapOf(1 to "one", 2 to "two")
println(map.mapValues { it.value.toUpperCase() }) // {1=ONE, 2=TWO}

// Keys and values are handled by their respective functions.
// filterKeys and mapKeys filter and transform Map keys
// filterValues and mapValues filter and transform Map values

all, any, count, find: apply judgment formula to set

All and any functions check whether all elements in the collection meet a certain condition (or its variant, whether there are elements that meet);
The count function checks how many elements satisfy the judgment formula;
The find function returns the first eligible element.

Bottom source code of all function:

/**
 * Returns `true` if all elements match the given [predicate].
 */
public inline fun <T> Iterable<T>.all(predicate: (T) -> Boolean): Boolean {
    if (this is Collection && isEmpty()) return true
    for (element in this) if (!predicate(element)) return false
    return true
}

From the source code of all function, it can be seen that only when all elements in the collection meet the conditions can true be returned, otherwise false will be returned.

    val list = listOf(Person("kerwin", 12), Person("bob", 19))

    val result = list.all { person: Person ->
        person.age >= 18
    }

println(result)
false

any function source code:

public inline fun <T> Iterable<T>.any(predicate: (T) -> Boolean): Boolean {
    if (this is Collection && isEmpty()) return false
    for (element in this) if (predicate(element)) return true
    return false
}

From any function, we can see that as long as one element in the collection meets the condition, it will return true, otherwise it will return false

    val list = listOf(Person("kerwin", 12), Person("bob", 19))

    val result = list.any { person: Person ->
        person.age >= 18
    }

println(result)
true

Bottom source code of count function:

public inline fun <T> Iterable<T>.count(predicate: (T) -> Boolean): Int {
    if (this is Collection && isEmpty()) return 0
    var count = 0
    for (element in this) if (predicate(element)) checkCountOverflow(++count)
    return count
}

From the count function, we can know: the number of elements in the set that satisfy the condition

    val result = list.count { person: Person ->
        person.age >= 10
    }

println(result)
2

Bottom source code of find function:

public inline fun <T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
    return firstOrNull(predicate)
}

public inline fun <T> Iterable<T>.firstOrNull(predicate: (T) -> Boolean): T? {
    for (element in this) if (predicate(element)) return element
    return null
}

It can be seen from the find function that: find the first element in the collection that meets the condition, find and return the element, otherwise return null

    val result = list.find { person: Person ->
        person.age >= 18
    }

println(result)
Person(name=bob, age=19)

groupBy: map that converts a list into a group

Bottom source code of groupBy function:

public inline fun <T, K> Iterable<T>.groupBy(keySelector: (T) -> K): Map<K, List<T>> {
    return groupByTo(LinkedHashMap<K, MutableList<T>>(), keySelector)
}

public inline fun <T, K, M : MutableMap<in K, MutableList<T>>> Iterable<T>.groupByTo(destination: M, keySelector: (T) -> K): M {
    for (element in this) {
        val key = keySelector(element)
        val list = destination.getOrPut(key) { ArrayList<T>() }
        list.add(element)
    }
    return destination
}

public inline fun <K, V> MutableMap<K, V>.getOrPut(key: K, defaultValue: () -> V): V {
    val value = get(key)
    return if (value == null) {
        val answer = defaultValue()
        put(key, answer)
        answer
    } else {
        value
    }
}

According to the groupBy function, all elements in the set are divided into different groups according to different characteristics, and the Map set is returned. key: grouping condition. value: List collection, each group is stored in a List.

    val list = listOf(
        Person("kerwin", 12),
        Person("bob", 19),
        Person("Alice", 12)
    )
 
    // Group people of the same age into groups
    val groupList = list.groupBy { person: Person ->
        person.age
    }

println(groupList)
{12=[Person(name=kerwin, age=12), Person(name=Alice, age=12)], 19=[Person(name=bob, age=19)]}

flatMap and flatten: handling elements in nested collections

The underlying source code of flatMap function:

public inline fun <T, R> Iterable<T>.flatMap(transform: (T) -> Iterable<R>): List<R> {
    return flatMapTo(ArrayList<R>(), transform)
}

public inline fun <T, R, C : MutableCollection<in R>> Iterable<T>.flatMapTo(destination: C, transform: (T) -> Iterable<R>): C {
    for (element in this) {
        val list = transform(element)
        destination.addAll(list)
    }
    return destination
}

From the flatMap function, we can see that each element is transformed (or mapped) according to the expression given by lambda (to return the iteratable subclass), and then multiple lists are merged (or tiled) into a list.

    val books = listOf(
        Book("java", listOf("abc", "bcd")),
        Book("kotlin", listOf("wer"))
    )

    // The authors of each book in the statistical collection merge into a flat list
    val bookAllAuthors = books.flatMap { book: Book ->
        book.authors
    }

println(bookAllAuthors)
[abc, bcd, wer]

3. Lazy set operation: sequence

When many chained set functions are called, such as map and filter, these functions will create intermediate sets as early as possible, that is to say, the intermediate results of each step are stored in a temporary list. Sequences avoid creating these temporary intermediate objects.

    // This creates temporary intermediate objects
    // Features: first call the map function on each element, then call the filter function on each element in the result list
    list.map {
            println("map: $it")
            it.name
        }
        .filter {
            println("filter: $it")
            it.startsWith("k")
        }
        .toList()

    // In order to improve efficiency, you can first turn operations into sequences rather than directly using sets 
    // Features: all operations are to finish processing the first element (mapping and filtering first), then the second element, and so on
    list.asSequence()  // Convert initial set to sequence
        .map {
            println("map: $it")
            it.name
        } // Sequence supports the same API as collection
        .filter {
            println("filter: $it")
            it.startsWith("k")
        }
        .toList()  // Convert result sequence back to list, reverse conversion

The entry of kotlin lazy collection operation is: Sequence interface, which represents a Sequence of elements that can be enumerated. It provides only one method, iterator, to get the value from the Sequence. The evaluation of elements in a Sequence is lazy, so using Sequence can perform chain operation on collection elements more efficiently.

public interface Sequence<out T> {
    /**
     * Returns an [Iterator] that returns the values from the sequence.
     *
     * Throws an exception if the sequence is constrained to be iterated once and `iterator` is invoked the second time.
     */
    public operator fun iterator(): Iterator<T>
}

Performing sequence operations: middle and end operations

Sequence operations fall into two categories: intermediate and terminal. An intermediate operation returns another sequence, a new sequence knows how to transform the elements in the original sequence; an end operation returns a result, which may be a set, element, number or any object obtained from the transformation sequence of the initial set.

    list.asSequence()
        .map(Person::name).filter { it.startsWith("k") }  // Intermediate operation, always inert
        .toList()  // Terminal operation

Create sequence

In addition to calling asSequence() on a collection to create a sequence, you can use the generateSequence function.

    // Calculate the sum of all natural numbers within 100
    val naturalNumbers = generateSequence(0) { it + 1 }
    val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }

// When the result sum is obtained, all delayed operations are performed
println(numbersTo100.sum()) // 5050

The takeWhile function is in the lower source code of the collection:

public inline fun <T> Iterable<T>.takeWhile(predicate: (T) -> Boolean): List<T> {
    val list = ArrayList<T>()
    for (item in this) {
        if (!predicate(item))
            break
        list.add(item)
    }
    return list
}

4. Using java functional interface

kotlin's lambda can seamlessly interoperate with Java APIs.

Pass lambda as a parameter to java method

You can pass a lambda to any method that expects a functional interface.

//java
void postponeComputation(int delay, Runnable computation)

//In kotlin, you can pass lambda as an argument to it, and the compiler will automatically convert it to a Runnable instance
// "A Runnable instance": refers to an instance of an anonymous class that implements the Runnable interface
// Only one instance of Runnable will be created in the whole program
postponeComputation(1000) {
    println("kotlin")
}

// Object expressions can also be passed as implementation of functional interfaces
// This method creates a new instance every time it is called
postponeComputation(1000,object : Runnable {
    override fun run() {
    }
})

// It is programmed as a global variable. There is only one instance in the program, and it is the same object every time it is called
val runnable = Runnable { println("kotlin") }
fun handleComputation() {
    postponeComputation(1000, runnable)
}

lambda captures variables from the scope surrounding it, and it is no longer possible to reuse the same instance every time it is called

fun handleComputation(id: String) {
    // lambda will capture the id variable
    // A new instance of Runnable is created each time handleComputation is called
    postponeComputation(1000) {
        println(id)
    }
}

Construction method of SAM: explicitly converting lambda into functional interface

The SAM constructor is a compiler generated function that lets you perform an explicit conversion from a lambda to a functional interface instance.

An interface with a single abstract method, called the SAM interface

If a method returns an instance of a functional interface, instead of returning a lambda directly, you need to wrap it with a SAM construction method

//  Return value using SAM construction method
// The SAM constructor has the same name as the underlying functional interface
// The SAM constructor takes only one parameter (a lambda used as a single abstract method body of a functional interface) and returns an instance of the class that implements the interface
fun callAllDoneRunnable() : Runnable {
    return Runnable { println("All Done.") }
}

callAllDoneRunnable().run()

SAM construction method can also be used to store the functional interface instance generated from lambda in a variable

   // Using SAM construction method to reuse listener instance
    val listener = OnClickListener { view ->
       // Use view.id to determine which button is clicked
        val text = when(view.id) {
            R.id.button1 -> "First Button"
            R.id.button1 -> "Second Button"
            else -> "Unknown Button"
        }
        toast(text)
    }
    
    button1.setOnClickListener(listener)
    button2.setOnClickListener(listener)

5. lambda with receiver: with and apply

A method of a different object can be called in the body of a lambda function without any additional qualifiers. Such a lambda is called a lambda with a receiver

with function

with function: it can be used to perform multiple operations on the same object without repeatedly writing out the name of the object.

// This code calls several different methods on the result instance, and the result name is repeated every time
fun alphabet(): String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
        result.append(letter)
    }
    result.append("\nNow I known the alphabet")
    return result.toString()
}

Use the with function to rewrite the previous code, and the with function takes two parameters.

// fun <T, R> with(receiver: T, block: T.() -> R): R
// Using with construction
fun alphabet(): String {
    val stringBuilder = StringBuilder()
    // Specifies the value of the recipient
    return with(stringBuilder) {
        for (letter in 'A'..'Z') {
            // Method to call receiver value through explicit this
            this.append(letter)
        }
        // Omit this to call methods
        append("\nNow I known the alphabet")
        // Return value from lambda
        this.toString()
    }
}

Using with and an expression function body to build the alphabet

// Use expression body syntax
fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I known the alphabet")
    toString()
}

apply function

The apply function always returns the object passed to it as an argument (in other words, the recipient object)

// Its receiver becomes the lambda receiver as an argument, and the result of apply ing is StringBuilder
fun alphabet() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I known the alphabet")
}.toString()

In java, it is usually completed by a separate Builder object; in kotlin, you can use the apply function on any object without any custom object.

// Initializing a TextView with apply
fun createViewWithCustomAttributes(context: Context) = TextView(context).apply {
    text = "Sample Text"
    textSize = 20.0
    setPading(10, 0, 0, 0)
}

You can use the kotlin standard library function buildString, which is responsible for creating StringBuilder and calling toString

fun alphabet() = buildString {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I known the alphabet")
}

Posted by wkrauss on Tue, 26 Nov 2019 04:10:20 -0800