Functional Programming in Kotlin (3)

命令式程式設計 VS 宣告式程式設計

Andy Lu
5 min readJul 28, 2022
Image by Antonio López from Pixabay

探討這兩種程式設計之前,我們先看一個範例,假設有一個列表,這個列表裡面儲存著每個學生的分數,其中包含數學、國語以及英文,如果我們希望將這個列表轉換成由姓名與數學分數組成的 Map,而我們只希望取前三名即可。

其中,這個列表裡面所儲存的類別為 Score,內容如下:

data class Score(val name: String, val math: Int, val english: Int, val chinese: Int)

val scores = listOf<Score>(
Score("Andy", 80, 70, 90),
Score("Colin", 70, 80, 90),
Score("Max", 84, 95, 70),
Score("Nina", 100, 40, 60),
)

根據前面的敘述,我們可以將這個需求寫成一個 Pseudo code。

fun sortMathToMap(list:List<Score>): Map<String, Int>{
val sortedList= sortMath()
val top3 = getTop3Items(sortedList)
val map = listToMap(top3)
return map
}

命令式 (Imperative) 程式設計

接下來,我們用最直覺的方式來完成這項需求:

fun sortMathToMap(list: List<Score>): Map<String, Int> {
val sortedByMath = list.sortedWith(compareBy { it.math }).reversed() //1
val scoreMap = mutableMapOf<String, Int>() //2
for(score in sortedByMath){ //3
if(scoreMap.size==3) break //4
scoreMap[score.name] = score.math
}
return scoreMap
}

在上方的程式碼中,我將前面 Pseudo code 的部分實作,首先我將這個列表依照數學成績作排列,這邊我是使用 sortedWith 函式,在這個函式裡面,我可以帶入比較的條件,所以我這邊帶入了數學分數 compareBy{ it.math } ,但是因為這個方式排出來的順序預設是由小排到大,所以我們在後方加上了 reversed() 來將列表逆轉(大到小)。

接著,由於我們需要將 List 轉換成 Map ,所以在函式裡我們宣告了一個可變的 Map (mutableMapOf<String, Int>()) 來存放結果。當我們需要處理一個列表的時候,我們需要使用一個可以遍訪所有元素的方式,在這邊我們選用了 for-loop 。因為我們只需要存取前三項,所以在迴圈的裡面我加上了判斷 scoreMap 的大小,當大小滿足我們所設定的值時,我們就結束迴圈。最後,就可以將這個 scoreMap 作為結果傳出去。

宣告式(Declarative)程式設計

我們將這個需求改成使用 FP 來實作,那結果會是如何呢?

fun sortMathToMap(list:List<Score>):Map<String, Int> {
return list.sortedByDescending { it.math }
.take(3)
.map { it.name to it.math }
.toMap()
}

我們可以發現,用 FP 改寫的程式碼,與我們所寫的 Pseudo code 蠻類似的,它的可讀性很高,我們可以很清楚的知道這個函式做了那些事情。

上面這段 FP 程式碼,還能將.map().toMap() 可以整合成一個函式 associate() 上面範例可以改成下面的樣子,讓這段程式碼更加精簡。

fun sortMathToMap(list:List<Score>):Map<String, Int> {
return list.sortedByDescending { it.math }
.take(3)
.associate { it.name to it.math }
}

我們在前面看到了兩種程式設計的方式,第一種為命令式 (Imperative) 程式設計,另一種則是宣告式 (Declarative) 程式設計。

雖然這兩種方式都能完成相同需求,這兩種設計方式除了程式碼數量有點不一樣,設計的理念也不太一樣。使用命令式程式設計的方式來完成,我們需要使用最底層的動作,如定義一個可變列表,用 for 迴圈來遍訪列表裡所有的項目,並且同時在迴圈裡面處理我們的需求。所以我們可以預想,如果我們有更多動作需要在迴圈裡面執行,那麼也就更容易出錯、不容易維護...等等。若改用宣告式程式設計來完成(FP),我們可以將每一個動作都切分成一行一行的指令,因為 Kotlin 的標準函式提供了許多高階函式,所以我們可以根據情境使用符合語意的函式,我們就能更好維護這個程式碼,在不同地方就不需要重複造輪子。

Focus on results over steps

Functional Thinking — Neal Ford

小結

命令式 (Imperative) 程式設計

在命令式程式設計中,我們專注在於函式「如何」完成這件事(How you to do),從上面的範例我們發現,我們會使用最底層的物件、操作來執行我們的運算、完成我們的需求。

宣告式(Declarative)程式設計

而宣告式與命令式最大的差異在於,函式是專注於「怎麼」完成這件事 (What you to do),我們能夠採用 Kotlin 標準函式庫提供的函式來組合,利用 Lambda 表達式將各自獨立的需求帶入,用更簡潔、直覺的方式來完成需求,程式碼也更容易維護。

如果這篇文章有幫助到您,拍手鼓勵我吧~

--

--

Andy Lu

Android/Flutter developer, Kotlin Expert, like to learn and share.