
Scala Tutorial(4)實戰 Higher-Order Function
話說上次我們看到了 Scala 是如何以物件導向的方式來實作出 first-class function,但並未提及要如何在 Scala 中活用 first-class function,以及 first-class function 在 Scala 中佔有何種角色,而這一篇我們就要探討這個問題。 雖然在 Scala 中 first-class function 可以被應用在許多地方,但最顯而易見的就是針對 Collection 的操作,因此我們將會先從 Java 如何操作 Collection 開始,並且借由對比,來看在 Scala 中如何使用 first-class function 來操作 Collection。 在 Java 中 Collection 的操作話說 Java 標準函式庫中提供了相當多的 Collection 供我們使用,例如 ArrayList、HashMap 或 HashSet 等等,這些 Collection 依照不同的資料結構來儲存資料,雖然有不一樣的特性,但大多數的 Collection 都提供了最基本的功能--Iteration,也就是走訪 Collection 中每一個元素的方式,而多數的時候我們會使用迴圈進行這樣的操作。 舉例而言,今天如果我們有一個 ArrayList,而我們希望將每一個元素都印出來時,我們可以這麼做: List<Integer> score = new ArrayList<Integer>(); // 將值放入 elements 中 for (int n: score) { System.out.println("score:" + n); } 而如果我們在程式碼的其他地方,需要再針對 不過通常現實世界裡的程式並不會那麼簡單,我們經常會需要針對 Collection 裡的元素做一些處理,並且也需要記錄處理的結果,這個時候我們可能很直覺地會將這個處理結果放到另放一個 Collection 中。 例如我們需要針對上述的 List<Integer> score = new ArrayList<Integer>(); List<Boolean> isPassed = new ArrayList<Integer>(); for (int n: score) { isPassed.add(n >= 60) } 如果仔細觀查的話,會發現我們在處理 Java 中的 Collection 時,經常都會用到迴圈,而且其組成和邏輯都是相同的:把 Collection 中的元素都走訪一遍。 但問題是,如果這些走訪邏輯這麼常被用到,而且長得都一模一樣,那為什麼我們每次都需要重覆撰寫這些迴圈,而不能把這個回圈的邏輯用函式包起來呢? 在 Scala 對 Collection 的操作沒錯,在 Scala 中正是這樣做的。由於這些迴圈的邏輯太過常用了,所以 Scala 的 Collection 直接提供了將這些迴圈的邏輯封裝起來的函式。 舉例而言,當我們要針對 Collection 中的每個元素做某件事情時,我們只要使用 val scores = List(93, 45, 95, 63, 58, 60, 73) // 分別在每一行印出 93 45 95, 63, 58, 60, 73 等值 scores.foreach((x: Int) => println(x)) 在這邊,我們傳給 foreach 這個函式的是一個 first-class function,這個 function 接受了一個 由於 foreach 會將這個函式套用在 List 中的每一個元素中,所以這隻程式執行之後,會將 scores 的每一個元素都印出來。 若您仍覺得難以理解,不如把上述的程式想像成以下的 Java 程式碼,唯一不同的是 private List<Integer> scores = new ArrayList<Integer>(); interface Function { public void apply(int n); } void foreach(Function t) { for (int n: scores) { t.apply(n); } } foreach(new Function() { public void apply(int n) { System.out.println(n); } }) 在這裡我們可以看到,foreach 只是走訪了 回到 Scala 的程式碼上,我們曾經提到過 Scala 有型別推論的功能,所以有許多地方型別的標註都可以省略,這裡也不例外。 由於在呼叫 foreach 的時候,Scala 已經知道 scores 裡的元素全都是整數,所以我們的 // 分別在每一行印出 93 45 95, 63, 58, 60, 73 等值 scores.foreach((x) => println(x)) 而在這個函式當中,其參數列表只有 // 分別在每一行印出 93 45 95, 63, 58, 60, 73 等值 scores.foreach(x => println(x)) 但如果你還是覺得寫成這樣子太麻煩,Scala 也提供了佔位符號的表示方法--反正 foreach 要的是一個函式,而這個函式的輸入值一定是 scores 裡的其中一個元素,為什麼還要那麼麻煩地特地取名叫做 // 分別在每一行印出 93 45 95, 63, 58, 60, 73 等值 scores.foreach(println(_)) 當然,至於要使用哪一種方式,則是單純看個人對於編程風格的偏好,上述三種表達方式事實上編譯出來的程式都是相同的。 filter 與 map除了走訪 Collection 中每一個元素用的 這個 function 接受一個參數,其參數的型別與 Collection 內元素的型別相同,而當將 Collection 的元素傳入的函式後,若返回的是 true 的話該元素會被留下,否則的話會被捨棄。 舉例而言,若我們要過濾 val scores = List(93, 45, 95, 63, 58, 60, 73) // 以下會印出三行 List(93, 95, 63, 60, 73) println(scores.filter((x: Int) => x >= 60)) // 傳入完整的 first-class function println(scores.filter(x => x >= 60)) // 省略參數型別 println(scores.filter(_ >= 60)) // 進一步省略參數名稱 // 會印出 List(93, 45, 95, 63, 58, 60, 73) println(scores) 在這邊要注意的,是最後一行 同樣的,關於 filter 這個函式,我們也可以用 Java 物件導向的方式去理解: interface Function { public boolean filter(int n); } private List<Integer> scores = new ArrayList<Integer>(); public List<Integer> filter(Function function) { List<Integer> newList = new ArrayList<Integer>(); for (n: scores) { if (function.apply(n)) { newList.add(n); } } } 在這邊我們依然可以看到,在 Java 的版本中我們是告訴 Java 「如何做」(走訪每個元素,執行檢查,符合資格的加入新的 List 中,不符合的不做事),但是在 Scala 的版本中我們比較偏向描述「意圖」,也就是「我們想要 x >= 60 的元素」,至於實際的執行方式則是交由 filter 函式處理。 至於 舉例來說,若我們要將一個字串列表中的字串全部轉成大寫,在 Java 中我們會這麼做: List<String> orignStrings = new ArrayList<String>(); List<String> upperCaseStrings = new ArrayList<String>(); for (String str: originStrings) { upperCaseStrings.add(str.toUpperCase()); } 但是因為我們已經知道建立新的 List 和迴圈的部份,Scala 已經幫我們代勞了,所以我們只需要告知 Scala 編譯器「我們要什麼」的核心邏輯就可以了,而這裡的核心邏輯則是「將元素轉成大寫」: val originStrings = List("Hello World", "This is a test", "Element 3") val upperCaseStrings = originStrings.map(_.toUpperCase) // 也可以寫成下面這樣 // val upperCaseStrings = originStrings.map(str => str.toUpperCase) // val upperCaseStrings = originStrings.map((str: String) => str.toUpperCase) // 印出 List(HELLO WORLD, THIS IS A TEST, ELEMENT 3) println(upperCaseStrings) 當然,傳入的函式的回傳值型別不一定要和原本的 Collection 中的元素相同,我們也可以透過 val originStrings = List("Hello World", "This is a test", "Element 3") val counter = originStrings.map(_.length) // 也可以寫成下面這樣 // val counter = originStrings.map(str => str.length) // val counter = originStrings.map((str: String) => str.length) // 印出 List(11, 14, 9) println(counter) 當他們合在一起另外,由於在 Scala 中 filter 和 map 這兩個函式都是定義在 Collection 上,而其回傳的又是另一個 Collection,所以我們可以使用 Fluent Interface 的風格來組合這些函式。 例如若我們可以挑選 List 中長度大於等於 4 的單字,並且將其轉換成大寫,然後再將每一個元素印出來: val words = List("a", "apple", "banana", "fruit", "car", "card", "dog", "bird") words.filter(_.length >= 4).map(_.toUpperCase).foreach(println _) 不知道讀者有沒有發現,不知不覺之間我們的程式碼好像和我們問題的描述可以進行一對一的對應關係耶: words.filter(_.length >= 4). // 挑選長度大於等於 4 的字串 map(_.toUpperCase). // 並且將其轉為大寫 foreach(println _) // 然後印出每一個元素 這正是 Higher-order function 應用在 Collection 上的好處,因為「如何走訪 Collection 中的元素」這件事已經從我們的程式碼中消失,所以程式碼除了變得更為簡潔外,意途的傳達也變得更明確了。 小結在這一篇中我們看到了 Scala 如何將 Functional Programming 中常出現的 first-class function,做為定義在 List 類別上的方法的參數,讓使用者可以不用自行處理常用的迴圈邏輯,而使得 Collection 的操作更加的簡潔,而程式碼的意圖也更為清楚。 不過到目前為止,我們都仍然只大致上介紹到「函式」的層次,但既然 Scala 是一個融合了 Functional Programming 與物件導向的程式語言,再加上我們也提過 Scala 很多關於 Functional Programming 的概念是實作在物件導向上,所以我們自然也不能忽略了物件導向的部份。 在下個星期,我們將會進入到 Scala 中物件導向的部份,並且看看 Scala 到底提供了哪些便利的功能,讓我們在寫物件導向的程式碼時能夠更輕鬆。 |
Aames Jiang
09/02
將會依照所傳入的 first-class function 來過率 Collection 內的元素
=>過濾
Aames Jiang
09/02
for (String str: originStrings) {
upperCaseStrings.add(originString.toUpperCase());
}
=>
upperCaseStrings.add(str.toUpperCase());