2019年3月25日

Kotlin Lambda

kotlin standard library 大量使用了 lambda 語法,最常見的就是用在 collections,另外也提供如何從 java 呼叫 kotlin lambda 的方式。最後說明一種特別的 lambda with receivers。

lambda expression

直到 Java 8 才提供了 lambda 語法。以下說明 lambda 的重要性

block of code as method parameters

如果要實作當某個事件發生時,執行一個 handler 的 code,以 java 來說可以用 anonymous inner class 實作。如果用 functional programming 的方式,可以將 function 當作 value,也就是將 function 當作參數傳遞給另一個 function,也就是把一小段 code 當作method 參數。

listener 實作 OnclickListener 介面,覆寫 onClick

/* Java */
button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View view) {
        /* actions on click */
    }
});

換成functional progeamming 的方式

button.setOnClickListener { /* actions on click */ }
lambda and collections

假設有個 list of data class: Person,需要找到裡面年紀最大的人,直覺會以 function 實作

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

fun findTheOldest(people: List<Person>) {
    var maxAge = 0
    var theOldest: Person? = null
    for (person in people) {
        if (person.age > maxAge) {
            maxAge = person.age
            theOldest = person
        }
    }
    println(theOldest)
}

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

kotlin 有提供這個 libray function: maxBy,maxBy 可用在任何一種 collection,後面的 {} 是 lambda implementation

>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.maxBy { it.age })
Person(name=Bob, age=31)

maxBy 也可以直接指定 member reference

people.maxBy(Person::age)
lambda expression 語法

lambda 語法是在 {} 裡面,前面是參數,後面是 body

val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2))
// 3

也可以直接呼叫 lambda

>>> { println(42) }()
42

但最好用 run 封裝起來,code 會比較容易閱讀

>>> run { println(42) }
42

剛剛的 people.maxBy { it.age } 其實原本應該寫成,使用 p:Person 這個參數,運算 p.age

people.maxBy({ p: Person -> p.age })

可以將 () 省略

people.maxBy { p: Person -> p.age }

將 p 的資料型別省略

people.maxBy { p -> p.age }

將 p 改成 lambda 的預設參數名稱 it

people.maxBy { it.age }

joinToString 也有在 kotlin 標準函式庫裡面,其中 transform 就是需要提供一個 lambda function

>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> val names = people.joinToString(separator = " ", transform = { p: Person -> p.name } )
>>> println(names)
Alice Bob

也可以簡化寫成這樣

people.joinToString(" ") { p: Person -> p.name }

lambda 語法裡面,也可以寫超過一個 expression 或 statement

val sum = { x: Int, y: Int ->
    println("Computing the sum of $x and $y")
    x + y
}
accessing variables

如果在 method 裡面宣告 anonymous inner class 時,可使用 method 裡面的 local 變數以及 parameters,lambda 也是一樣。

messages.forEach 裡面的 lambda function 使用了外部 method 的參數 $prefix

fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
    messages.forEach {
        println("$prefix $it")
    }
}

fun main(args: Array<String>) {
    val errors = listOf("403 Forbidden", "404 Not Found")
    printMessagesWithPrefix(errors, "Error:")
}

kotlin 跟 java 最大的差異是 kotlin 沒有限制只能使用 final variables,也可以在 lambda function 裡面修改變數的值。

fun printProblemCounts(responses: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    responses.forEach {
        if (it.startsWith("4")) {
            clientErrors++
        } else if (it.startsWith("5")) {
            serverErrors++
        }
    }
    println("$clientErrors client errors, $serverErrors server errors")
}

fun main(args: Array<String>) {
    val responses = listOf("200 OK", "418 I'm a teapot",
                           "500 Internal Server Error")
    printProblemCounts(responses)
}
member references

如果 lambda 要傳送一個已經定義好的 function 時,要使用 :: operator,將 function 轉換為 value,這種語法稱為 member reference,前面是 class,後面是 method 或是 property

val getAge = Person::age

等同

val getAge = { person: Person -> person.age }

如果是 top-level function,就不用寫 class name

fun salute() = println("Salute!")

fun main(args: Array<String>) {
    run(::salute)
}

將 lambda function 轉送給 sendEmail function

val action = { person: Person, message: String ->
    sendEmail(person, message)
}

直接使用 member reference

val nextAction = ::sendEmail

也可以將 member reference 套用在 class constructor

>>> data class Person(val name: String, val age: Int)
>>> val createPerson = ::Person
>>> val p = createPerson("Alice", 29)
>>> println(p)
Person("Alice", 29)

也可以使用 extension function

fun Person.isAdult() = age >= 21
val predicate = Person::isAdult

collection 使用的 functional APIs

filter and map

取得偶數的元素,it 是 lambda 的預設變數名稱

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3, 4)
    println(list.filter { it % 2 == 0 })
}

取得年齡超過30歲的 Person

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

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    println(people.filter { it.age > 30 })
}

取得每一個元素的平方 list

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3, 4)
    println(list.map { it * it })
}

取得所有人的名字 list

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

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    println(people.map { it.name })
}

也可以寫成people.map(Person::name)

可以將 filter 與 map 連在一起 people.filter { it.age > 30 }.map(Person::name)

找到年齡最大的 personpeople.filter { it.age == people.maxBy(Person::age).age } 但因為 people.maxBy(Person::age).age 重複做了很多次,所以將這兩個分開做,效能會比較好

val maxAge = people.maxBy(Person::age).age
people.filter { it.age == maxAge }

使用 filter 及 transformation functions to maps

fun main(args: Array<String>) {
    val numbers = mapOf(0 to "zero", 1 to "one")
    println(numbers.mapValues { it.value.toUpperCase() })
}

//{0=ZERO, 1=ONE}
all, any, count, find

檢查是不是所有元素都符合某個條件

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

val canBeInClub27 = { p: Person -> p.age <= 27 }

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 27), Person("Bob", 31))
    println( people.all(canBeInClub27) )
}

檢查是否至少有一個元素符合某個條件

println(people.any(canBeInClub27))

!allany 的意義是相反的,可以互換

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(!list.all { it == 3 })
    println(list.any { it != 3 })
}

count 計算數量

al people = listOf(Person("Alice", 27), Person("Bob", 31))
println(people.count(canBeInClub27))

用 find 找到一個符合條件的元素

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

val canBeInClub27 = { p: Person -> p.age <= 27 }

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 27), Person("Bob", 31))
    println(people.find(canBeInClub27))
}
groupBy: 將 list 轉換為 map of groups

將 list 以 age 分組

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

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 31),
            Person("Bob", 29), Person("Carol", 31))
    println(people.groupBy { it.age })
}

執行結果為 Map<Int, List<Person>>

{31=[Person(name=Alice, age=31), Person(name=Carol, age=31)], 29=[Person(name=Bob, age=29)]}

以 string 的第一個 character 分組

fun main(args: Array<String>) {
    val list = listOf("a", "ab", "b")
    println(list.groupBy(String::first))
}

執行結果

{a=[a, ab], b=[b]}
flatMap, flatten

flatMap 做兩件事:根據參數,轉換 (maps) 每一個 element 為 collection,合併 (flattens) 數個 lists 為一個

fun main(args: Array<String>) {
    val strings = listOf("abc", "def")
    println(strings.flatMap { it.toList() })
}

執行結果

[a, b, c, d, e, f]

set of all authors

class Book(val title: String, val authors: List<String>)

fun main(args: Array<String>) {
    val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")),
                       Book("Mort", listOf("Terry Pratchett")),
                       Book("Good Omens", listOf("Terry Pratchett",
                                                 "Neil Gaiman")))
    println(books.flatMap { it.authors }.toSet())
}

執行結果

[Jasper Fforde, Terry Pratchett, Neil Gaiman]

lazy collection operations: sequences

sequence 不同於 collection,在使用到該物件時才會進行運算,而不是在定義時,就馬上進行運算,也不會產生 intermediate temporary objects

asSequence 是讓 collection 轉成 sequence,toList 則是讓 sequence 轉回 collection

fun main(args: Array<String>) {
    val seq = listOf(1, 2, 3, 4).asSequence()
            .map { print("map($it) "); it * it }
            .filter { print("filter($it) "); it % 2 == 0 }

    println( "after seq")
    val list = seq.toList()

    println( )
    println( list )
}

執行結果

after seq
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16) 
[4, 16]

在 sequence 的 operations 中,map 及 filter 是 intermediate operations,toList 是 terminal operation,一直到 terminal operation 才會進行運算

sequence.map{...}.filter{...}.toList()

除了 asSequence() 還可以用 generateSequence() 產生 sequence,一直到呼叫 sum 的時候,才會進行運算

fun main(args: Array<String>) {
    val naturalNumbers = generateSequence(0) { it + 1 }
    val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
    println(numbersTo100.sum())
}

另一個常用的是 sequence of parents,以下是檢查 file 是否在某一個隱藏的目錄中,所以要產生 sequence of parent directories,然後檢查是否有任一個為 hidden

import java.io.File

fun File.isInsideHiddenDirectory() =
        generateSequence(this) { it.parentFile }.any { it.isHidden }

fun main(args: Array<String>) {
    val file = File("/Users/svtk/.HiddenDir/a.txt")
    println(file.isInsideHiddenDirectory())
}

using Java functional interfaces

Kotlin lambdas 可跟 Java APIs 一起使用

在 java 的 setOnClickListener

button.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        ...
    }
}

在 kotlin

button.setOnClickListener { view -> ... }

其中 OnClickListener 稱為 funcational interface 或是 SAM(single abstract method) interfaces

public interface OnClickListener {
    void onClick(View v);
}
passing a lambda as a parameter to a java method

將 lambda 傳到需要 functional interface 的 java method

/* Java */
void postponeComputation(int delay, Runnable computation);

在kotlin 可這樣呼叫,compiler 會自動產生一個 instance of Runnable,也就是 instance of an anonymous class implements Runnable

postponeComputation(1000) { println(42) }

compiler 會用 run method 產生 instance of anonymous class that implements Runnable

postponeComputation(1000, object: Runnable {
    override fun run() {
        println(42)
    }
})

每一個呼叫 handleComputation 都會產生一個新的 Runnable instance 儲存 id 欄位

fun handleComputation(id: String) {
    postponeComputation(1000) {
        println(id)
    }
}
SAM constructors: explicit conversion of lambdas to functional interfaces

SAM constructor 是 compiler 產生的 function,可轉換 lambda 為 instance of functional interface

我們不能直接 return lambda,但可包裝在 SAM constructor 裡面

fun createAllDoneRunnable(): Runnable {
    return Runnable { println("All done!") }
}

fun main(args: Array<String>) {
    createAllDoneRunnable().run()
}

例如 Android 的 listener,可以產生出來給多個 button 使用

val listener = OnClickListener { view ->
    val text = when (view.id) {
        R.id.button1 -> "First button"
        R.id.button2 -> "Second button"
        else -> "Unknown button"
    }
    toast(text)
}
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)

lambda with receivers: with and apply

以下這是 kotlin 的 lambda 的功能,不能在 java 使用。

可以在 lambda 呼叫 methods of a different object,不需要額外的 qualifiers

with

以下是列印英文字母的範例

fun alphabet(): String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
         result.append(letter)
    }
    result.append("\nNow I know the alphabet!")
    return result.toString()
}

fun main(args: Array<String>) {
    println(alphabet())
}

用 with 改寫

fun alphabet(): String {
    val stringBuilder = StringBuilder()
    // 指定 receiver value
    return with(stringBuilder) {
        for (letter in 'A'..'Z') {
            // 以 this 呼叫 receiver value 的 method
            this.append(letter)
        }
        // 呼叫 method,但省略 this
        append("\nNow I know the alphabet!")
        // 由 lambda 回傳 value
        this.toString()
    }
}

fun main(args: Array<String>) {
    println(alphabet())
}

省略 this

fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
    toString()
}

fun main(args: Array<String>) {
    println(alphabet())
}
apply

with 的 value 會回傳 lambda code 的最後一個 expression,但有時需要 return receiver object,而不是 lambda 運算結果,這時要改用 apply

apply 用起來就像是 with,差別是 apply 會回傳傳入作為參數的 object

fun alphabet() = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}.toString()

fun main(args: Array<String>) {
    println(alphabet())
}

也可以改用 buildString,buildString 會處理 StringBuffer,也會呼叫 toString,buildString 是 lambda with a receiver,且 receiver 是 StringBuilder

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

fun main(args: Array<String>) {
    println(alphabet())
}

References

Kotlin in Action

沒有留言:

張貼留言