如何使用高階函式和Lambda(Higher-order Functions and Lambdas)

如何使用高階函式和Lambda(Higher-order Functions and Lambdas)

情境

我們來討論一下 Kotlin 的 Functions,一般來說,函式在 Kotin 裡面是屬於 First-class object (第一類物件),什麼是第一類物件? 簡單來說,就是函式可以傳進函式當作參數,也可以回傳函式當作參數值,這類的程式語言特性就稱之為 Functional Programming。

說明

基本上 Function 分成三大類。

  • 一般函式(Introduction Functions)

  • 高階函式(Higher-order Functions and Lambdas)

  • 行內函式(Inline Functions)

一般的函式有哪些?

Member Function

很一般的函式,通常可以選擇傳或不傳入一個參數,並且可以設計有或沒有回傳值,如果有回傳值,則最後會用 retrun 的方式回傳對應的值。

class Sample() {
    fun foo() { print("Foo") }
}

Tail Recursive Functions

遞迴函式是一種很常見型態,它會在內部再次的呼叫自己,直到條件滿足為止。

fun recursiveFactorial(n: Long) : Long {
    return if (n <= 1) {
        n
    } else {
        n * recursiveFactorial(n - 1)
    }
}

Generic Functions

泛型對於方法來說是一種很方便的處理方式,如果沒有泛型,不同型態之間就必須寫入多個方法來實現。

fun <T> singletonList(item: T): List<T> { 
	/*...*/ 
}

Extension Functions

這邊的延伸方法,就是在既有的資料結構下,新增該類別的一個新方法。

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]
    this[index2] = tmp
}

Higher-order Functions

只要傳入函式或者回傳函式或者兩者皆是,則代表它是高階函式。

fun runTransformation(f: (String, Int) -> String): String {
    return f("hello", 3)
}

Inline Functions

行內函式是 Kotlin 的一種特殊存在,它可以讓執行效率變好,後續會再談。

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

而這當中我們要著墨的重點在於高階函式,什麼是高階函式呢?

任何以 Lambda 或者函式引用作為參數的函式,或者返回值為 Lambda 或函式引用,或者兩者都滿足的皆稱為高階函式。

我們來說明一下什麼是 Lambda?

Lambda 就是可以傳遞給其他函式的一段程式碼。

通常我們都會這樣來進行表示,比較像是 Java 中的匿名函式(anonymous function)。

view.setOnClickListener{
  	//do some thing
}

Collection API

在 Kotlin 中,在集合類別提供了許多 API 可以讓你輕鬆操作集合,以下就舉幾個常見的例子來做說明。

  • foreach
  • filter
  • map
  • reduce
  • flatmap

foreach

foreach 顧名思義就是每一個都跑過一次,所以下面程式可以看到輸出列表中所有的人名,其中,Lambda 中是可以拿來判斷要輸出什麼資訊,這個通常都會被拿來當作 for 迴圈使用。

val people = listOf(Person("Alice", 29), Person("Bob", 31))
people.foreach{
    print(it.name) // output : Alice Bob
}

可以看到下圖,從左邊到右邊,每個元素都逐一盤點。

filter

從字面上的意思就可以知道叫做過濾器,因此,我們在 Lambda 中就可以設定條件來選出要的物件。

val people = listOf(Person("Alice", 29), Person("Bob", 31))
val pList = people.filter{
    it.age > 30
}
print(pList) // output: [Person(name=Bob, age=31)]

從下圖得知,如果要過濾正方形,就將條件設定為正方形通過,那麼出來的結果就會是正方形的列表了。

map

map 可以解釋成對應或者轉化,把某一個列表轉化成你想要變成的形狀。

val people = listOf(Person("Alice", 29), Person("Bob", 31))
val pList = people.map{
	it.name
}
print(pList) // output: [Alice, Bob]

從下圖可以清楚看出我們原本列表有的東西,將其整理為新的列表,或者將原本列表沒有的東西轉化成相同的東西。

reduce

reduce 通常會搭配一個 map 來進行操作,它的意思很像把 map 轉換完畢的列表歸納 (reduce) 成一個結果。

val people = listOf(Person("Alice", 29), Person("Bob", 31))
val pList = people.map{
	it.name
}.reduce{ acc, s-> 
	"$acc,$s"
}
print(pList) // output: Alice,Bob

從下圖我們可以看到,當 map 結束以後,我們將所有的人名都加入一個逗號 (,) 來進行區隔。

flatmap

flatmap 這個操作比較難以理解,其實你只需要想像它把一個二維陣列攤平變成一維陣列,就大概能夠了解它的意義了。

val people = listOf(
	Person("Alice", 29, arrayListOf("A", "B")), 
	Person("Bob", 31, arrayListOf("C", "D")))
val books = people.flatmap{
	it.bookList
}
print(books) // output: [A, B, C, D]

從下圖就可以很容易看出他的觀念。

Eager evaluation vs Lazy Evaluation

  • 及早求值 (Eager evaluation):代表著每次執行 filter 或者 map 等高階函式,會立刻回傳一個列表。

  • 惰性求值 (Lazy Evaluation):透過 asSequence 這個方法,讓最末端的操作才將值輸出。

在下面的程式碼,中間 map 以及 filter 都會立刻回傳一個列表,再透過這個列表進行下一次的運算,這樣一來,我們如果進行多個操作,則會在記憶體內產生許多的列表。

people.map(People::name).fliter{ it.startWith("A")}

透過以下程式碼的改良,我們就可以在最後末端操作才輸出列表,末端 toList 稱之為終端操作。

people.asSequence()
	.map(Person::name)
	.filter { it.startsWith("A") }
	.toList()

Collection vs Sequences

這邊官方部落格有更詳細的解說,以下圖片是從官方部落格節錄出來的,它說明了惰性求值優於急性求值。

Switch Position

有時候我們在進行高階函式操作的時候,更換位置也可以改善一些效能問題。

people.map(People::name).filter{ it.name.startWith("A")}

在這個範例中,我們只需要將 map 以及 filter 的位置進行更換,馬上就可以將 map 的數量篩選掉一些。

people.filter{ it.name.startWith("A")}.map(People::name)

以上就是簡單的高階函式操作了。