[Day 11] 遠征 Kotlin × 函數式程式設計

函數式程式設計特性

我們在前面函數章節有提到 Lambda 的基本概念,而如果我們想要更好運用 Lambda 語法與相關函數API,可以先認識函數程式設計(Functional Programming, 簡稱 FP)會有非常大幫助,FP 是一種程式設計方法,與前面章節提到的物件導向程式設計(Object-Oriented Programming)是不同的設計概念,兩種設計的思考方式有許多不同,FP 主要會有以下特性:

  • 會依賴於前面函數章節提到的高階函數(Higher-Order Functions)所傳回的結果,所謂高階函數即為利用函數作為參數或返回值的方法
  • 函數必須符合第一類物件(First-Class-Object)原則
  • 保持純函數(Pure functions)特性,即函數在執行時,不會有任何副作用(Side Effect)的狀況,無副作用是指函數內部不會影響到函數外部的任何狀態
  • 保持 immutable 特性,即資料一經賦值後就不能被修改,重視函數回傳結果(Output),不修改傳入的參數狀態(Input)

函數類別

一般我們在使用 FP 設計方法時,通常會由三種函數所構成-轉換(Transform)過濾(Filter)合併(Combine),每種函數目標都是為了取得最終結果進行設計,而函數彼此間可以互相配合使用,代表我們可以利用這樣的特性關係,將多個函數進行組合,處理複雜的計算行為。

轉換(Transform)是指我們會將輸入(Input)參數利用轉換器進行特定條件處理,再回傳處理的新結果,在 Kotlin 中常使用的轉換函數為 mapflatMap,可參考以下範例:

1
2
3
4
5
6
7
fun main() {
val data = listOf<Int>(1, 2, 3)
val result = data.map { it * 2}
println(result)

// 印出 [2, 4, 6]
}

過濾(Filter)則是具有過濾符合特定條件的作用,一般會配合條件運算式(predicate)函數,利用此函數判斷傳入參數是否符合條件判斷,依照判斷結果回傳 true 或 false,若為 true,則將元素加入返回的新集合內,我們可以運用 filter 函數進行過濾處理,可參考以下範例:

1
2
3
4
5
6
7
8
9
10
fun main() {
// 建立原始資料
val data = listOf<Int>(1, 2, 3)
// 進行過濾的結果資料
val result = data.filter { it > 1 }
.map { it * 2}
println(result)

// 印出 [4, 6]
}

合併(Combine)將不同資料或不同集合組合成一個新集合,我們可以運用 zip 合併函數進行合併處理,可參考以下範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() {
// 建立員工資料
val personData = listOf<String>("Devin", "Eric", "Mary")
// 建立薪資資料
val salaryData = listOf<Int>(1300, 1500, 1200).map { it * 5 }
// 組合員工與薪資資料
val result = personData.zip(salaryData).toMap()
// 印出員工對應的薪水資料
println(result["Devin"])
println(result["Eric"])
println(result["Mary"])

// 印出 6500, 7500, 6000
}

標準函數

在 Kotlin 標準函式庫中有提供一些支援 lambda 的標準函數-Scope Function,如 applyletrunalsotakeIf等五種常用函數,若能善用這些函數進行開發,會讓我們的程式增加可讀性,以下分別進行介紹:

  • apply

    apply 函數可視為配置函數,將需要設定的接收者傳入,再針對需求進行函數設定,例如以下範例:

    1
    2
    3
    4
    5
    6
    7
    8
    fun main() {
    // 使用 apply 函數,可更直觀的方式進行設定
    val fileUsingApply = File("data.txt").apply {
    setReadable(true)
    setWritable(true)
    setExecutable(false)
    }
    }
  • let

    let 函數可以產生一個暫時變數(預設為 it)作用於 lambda 運算式,let 只會將最後一行作為返回值(lambda 結果值)進行回傳,例如以下範例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fun main() {
    // 建立一個數值集合
    val data: List<Int> = listOf<Int>(4, 5, 6)
    // 取得集合第一個資料並使用 let 函數進行相乘
    val result = data.first().let { it * it }
    println(result)

    // 印出結果為 16
    }
  • run

    run 函數與 apply 函數相似,差別在於 run 函數不會返回接收者,返回的是一個 Lambda 結果,例如以下範例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fun main() {
    val data: List<Int> = listOf<Int>(4, 5, 6)
    val result = data.first() // 取得集合第一個資料
    .let { it * it } // 利用 let 函數進行相乘
    .run { this == 16 } // 利用 run 函數判斷結果值是否等於 16
    println(result)

    // 印出結果為 true
    }
  • also

    also 函數與 let 函數相似,差別在於 also 函數返回的是接收者,而 let 函數返回的是 Lambda 結果,參考以下範例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    fun main() {
    val data: List<Int> = listOf<Int>(4, 5, 6)
    // 取得集合第一個資料並使用 also 函數進行內部計算
    val result = data
    .first()
    .also {
    val calculateResult = it * it
    println("相乘計算結果 $calculateResult")
    }
    println("返回結果:$result")

    // 印出結果為
    // 相乘計算結果 16
    // 返回結果 4 -> 代表 also 是回傳原接收者物件
    }
  • takeIf

    takeIf 與前面介紹的函數有些不同,takeIf 函數必須與 Lambda 提供的條件運算式(predicate)函數進行搭配使用,如果條件運算式成立結果為 true,則 takeIf 函數則會回傳原接收者物件,反之,若為 false,就會回傳 null,可參考以下範例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fun main() {
    val data: List<Int> = listOf<Int>(4, 5, 6)

    val result = data.first() // 取得集合第一個資料
    .run { this + 4 } // 利用 run 函數進行數值加 4,
    .takeIf { it == 8 } // 利用 takeIf 函數搭配判斷運算式,若數值符合則回傳計算值,不符合則回傳 null
    println("返回結果 $result")

    // 印出「返回結果 8」
    }

結論

在這篇提到函數程式設計的基本概念、三種函數設計與 Lambda 相關函數介紹,後續也會逐漸在 Spring Boot 章節進一步介紹實際運用。這邊也希望大家能夠理解函數程式設計只是一種設計方法,而既然是設計方法,就不會有所謂的好壞之分,只有應用場景是否適合的差別,而 Kotlin 可支援多種程式設計方法,有時候我們也會混用物件導向程式設計與函數式程式設計解決手上的專案需求。

Reference