2019/4/15

Kotlin Higher-order functions: lambdas as parameters and return values


higher-order functions: 自訂 function 且使用 lambda 為 parameters 或是 return values


inline functions: 可減輕使用 lambda 的 performance overhead


宣告 higher-order functions


例如 list.filter { x > 0 } 以 predicate function 為參數,就是一種 higher-order function


Function types

要知道如何定義 lambda 為參數的 function


我們已經知道,再不需要定義 type 也能使用 lambda


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

compiler 是將程式轉換為


// 以兩個 Int 為參數,回傳 Int
val sum: (Int, Int) -> Int = { x, y -> x + y }
// 沒有參數,沒有 return value
val action: () -> Unit = { println(42) }

val sum: (Int, Int) -> Int 其中 (Int, Int) 是 parameter types,後面的 Int 是 return type


如果可以回傳 null,就這樣定義


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

宣告為 funcation type with a nullable return type


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



callback 是有參數名稱的 function type


fun performRequest(
       url: String,
       callback: (code: Int, content: String) -> Unit
) {
    /*...*/
}

fun main(args: Array<String>) {
    val url = "http://kotl.in"
    performRequest(url) { code, content -> /*...*/ }
    performRequest(url) { code, page -> /*...*/ }
}

calling functions passed as arguments

twoAndTree 的參數中 operation: (Int, Int) -> Int 是 function type


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

fun main(args: Array<String>) {
    twoAndThree { a, b -> a + b }
    twoAndThree { a, b -> a * b }
}



另一個例子


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()
}

fun main(args: Array<String>) {
    println("ab1c".filter { it in 'a'..'z' })
}

fun String.filter(predicate: (Char) -> Boolean): String


  • 前面的 String 是 receiver type
  • predicate 是 parameter name
  • (Char) -> Boolean 是 function type

Using function type from Java

kotlin 宣告的有 function type 為參數的 function


/* Kotlin declaration */
fun processTheAnswer(f: (Int) -> Int) {
    println(f(42))
}

在 Java 可用 lambda 使用


public class ProcessTheAnswerLambda {
    public static void main(String[] args) {
        processTheAnswer(number -> number + 1);
    }
}

舊版 java 要用 anonymous class


import static ch08.ProcessTheAnswer.ProcessTheAnswer.*;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;

/* Java */

public class ProcessTheAnswerAnonymous {
    public static void main(String[] args) {
        processTheAnswer(
            new Function1<Integer, Integer>() {
                @Override
                public Integer invoke(Integer number) {
                    System.out.println(number);
                    return number + 1;
                }
            });
    }
}



在 java 使用 forEach


import java.util.ArrayList;
import java.util.Collections;
import kotlin.Unit;
import kotlin.collections.CollectionsKt;
import java.util.List;

/* Java */

public class UsingForEach {
    public static void main(String[] args) {
        List<String> strings = new ArrayList();
        strings.add("42");
        CollectionsKt.forEach(strings, s -> {
           System.out.println(s);
           return Unit.INSTANCE;
        });
    }
}

default and null values for parameters with function types

先前 joinToString 的實作


fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = "",
        transform: (T) -> String = { it.toString() }
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(transform(element))
    }

    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() }))
}

缺點是 transform 裡面永遠會使用 toString 轉換字串


修改為 nullable parameter of a function type


fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = "",
        transform: ((T) -> String)? = null
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        val str = transform?.invoke(element)
            ?: element.toString()
        result.append(str)
    }

    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() }))
}

returning functions from functions

計算 cost of shipping depending on the selected shipping method,實作 logic variant,回傳對應的 function


enum class Delivery { STANDARD, EXPEDITED }

class Order(val itemCount: Int)

// 宣告回傳 function 的 function
fun getShippingCostCalculator(
        delivery: Delivery): (Order) -> Double {
    if (delivery == Delivery.EXPEDITED) {
        // return lambdas from the function
        return { order -> 6 + 2.1 * order.itemCount }
    }

    return { order -> 1.2 * order.itemCount }
}

fun main(args: Array<String>) {
    val calculator =
        getShippingCostCalculator(Delivery.EXPEDITED)
    // 呼叫 returned function
    println("Shipping costs ${calculator(Order(3))}")
}



另一個例子,GUI contract management application,需要根據 UI 狀態,決定要顯示哪些 contracts


data class Person(
        val firstName: String,
        val lastName: String,
        val phoneNumber: String?
)

class ContactListFilters {
    var prefix: String = ""
    var onlyWithPhoneNumber: Boolean = false

    // 宣告產生 function 的 function
    fun getPredicate(): (Person) -> Boolean {
        val startsWithPrefix = { p: Person ->
            p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)
        }
        if (!onlyWithPhoneNumber) {
            return startsWithPrefix
        }
        return { startsWithPrefix(it)
                    && it.phoneNumber != null }
    }
}

fun main(args: Array<String>) {
    val contacts = listOf(Person("Dmitry", "Jemerov", "123-4567"),
                          Person("Svetlana", "Isakova", null))
    val contactListFilters = ContactListFilters()
    with (contactListFilters) {
        prefix = "Dm"
        onlyWithPhoneNumber = true
    }
    println(contacts.filter(
        contactListFilters.getPredicate()))
}

removing duplication through lambdas

analyzes visits to a website: SiteVisit 儲存 path of each visit, duration, OS


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

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

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

要計算windows 的平均使用時間,直接寫一個 function


val averageWindowsDuration = log
    .filter { it.os == OS.WINDOWS }
    .map(SiteVisit::duration)
    .average()

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

改寫,讓 averageDurationFor 可傳入 OS 參數


fun List<SiteVisit>.averageDurationFor(os: OS) =
        filter { it.os == os }.map(SiteVisit::duration).average()

fun main(args: Array<String>) {
    println(log.averageDurationFor(OS.WINDOWS))
    println(log.averageDurationFor(OS.MAC))
}

同時計算 ios, android


val averageMobileDuration = log
    .filter { it.os in setOf(OS.IOS, OS.ANDROID) }
    .map(SiteVisit::duration)
    .average()

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

以 function 動態調整過濾的條件


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

fun main(args: Array<String>) {
    println(log.averageDurationFor {
        it.os in setOf(OS.ANDROID, OS.IOS) })
    println(log.averageDurationFor {
        it.os == OS.IOS && it.path == "/signup" })
}

inline function: 減輕 lambda 的 overhead


how inlining works

宣告 function 為 inline,就表示會直接替代 code,而不是用呼叫 function 的方式


以下是 synchronized 的做法,鎖定 lock 物件,執行 action,最後 unlock


inline fun <T> synchronized(lock: Lock, action: () -> T): T {
    lock.lock()
    try {
        return action()
    } finally {
        lock.unlock()
    }
}

val l = Lock()
synchronized(l) {
    // ...
}

因宣告為 inline,以下這些 code


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

synchronized(l) 會轉換為


    l.lock()
    try {
        println("Action")
    } finally {
        l.unlock()
    }

inline function 的限制

並非每一個使用 lambdas 的 function 都可以改為 inline


當 function 為 inlined,body of the lambda expression 傳入當作參數,會直接替換為 inline function,如果 function 直接被呼叫,就可以 inline,但如果需要儲存 function,後面才要使用,就不能用 inline


inlining collection operations

kotlin 的 filter 是定義為 inline, map 也是,因為 inline,就不會產生額外的 classes or objects


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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun main(args: Array<String>) {
    println(people.filter { it.age < 30 })
    
    println(people.filter { it.age > 30 }.map(Person::name))
}

什麼時候要用 inline functon

使用 inline 只會在 function 使用 lambda 為參數的狀況下,改進效能


JVM 有支援 inlining support,他會分析 code,並嘗試 inline code


除了避免產生多餘的 class for each lambda 及 object for the lambda instance,另外 JVM 還不夠聰明,能夠找出所有可以 inline 的狀況


Using inlined lambdas for resource management

另一個 lambda 能減少重複程式碼的狀況是 resource management,取得 Resource -> 使用 -> release


Resource 可能是 file, lock, database transaction ....


通常會用 try/finally statement 包裝起來


kotlint 提供 "withLock" function,可處理 synchronized 的工作


val l: Lock = ...

l.withLock {
    // access the resource protected by this lock
}

withLock 是這樣實作的


fun <T> Lock.withLock(action: () -> T): T {
    lock()
    try {
        return action()
    } finally {
        unlock()
    }
}

java 提供 try-with-resources statement


/* Java */
static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br =
        new BufferedReader(new FileReader(path))) {
            return br.readLine();
    }
}

kotlin 沒有對應的語法,但可以用 "use" 搭配 lambda 提供相同的功能


import java.io.BufferedReader
import java.io.FileReader
import java.io.File

fun readFirstLineFromFile(path: String): String {
    BufferedReader(FileReader(path)).use { br ->
        return br.readLine()
    }
}

control flow in higher-order functions


return statements in lambdas: return from an enclosing function

在 list of Person 裡面尋找 Alice


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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

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

fun main(args: Array<String>) {
    lookForAlice(people)
}

將 for 改為 forEach,在 lambda function 中留下 return,這個 return 是 non-local return,這種 return 只對 function 使用 inlined lambda function 為參數有作用


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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

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

fun main(args: Array<String>) {
    lookForAlice(people)
}

因為 forEach 的 lambda function 是用 inlined,因此可使用 return


returning form lambdas: return with label

local return 類似 break 的功能,會中斷 lambda 的執行,


區分 local return 與 non-local,可使用 labels,可將 lambda expression 加上 label,並在 return 時參考此 label


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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach label@{
        if (it.name == "Alice") {
            println("Found!")
            return@label
        }
    }
    // 永遠會執行這一行
    println("Alice might be somewhere")
}

fun main(args: Array<String>) {
    lookForAlice(people)
}



也可以用 lambda 做為 label


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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

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

fun main(args: Array<String>) {
    lookForAlice(people)
}



也可以將 this 加上 label


fun main(args: Array<String>) {
    // implicit receiver 為 this@sb
    println(StringBuilder().apply sb@{
       listOf(1, 2, 3).apply {
           // this 參考到 closest implicit receiver in the scope
           this@sb.append(this.toString())
       }
    })
}

anonymous functions: local returns by default

使用 anonymous function 替代 lambda expression


return 會參考到 closest functon: 也就是 anonymous function


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

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List<Person>) {
    people.forEach(fun (person) {
        if (person.name == "Alice") return
        println("${person.name} is not Alice")
    })
}

fun main(args: Array<String>) {
    lookForAlice(people)
}

另一個例子


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

people.filter(fun (person) = person.age < 30)

References


Kotlin in Action

沒有留言:

張貼留言