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

沒有留言:

張貼留言