您好,登錄后才能下訂單哦!
前言
當垃圾回收成為主流時,它消除了所有類別的難以調試的問題,使運行時能夠為開發人員管理復雜的、容易出錯的進程。函數式編程旨在為您編寫的算法實現同樣的優化,這樣您就可以從一個更高的抽象層面開展工作,同時運行時執行復雜的優化。
Java 下一代語言并不都占用從命令式到函數式的語言頻譜的同一位置,但都展現出函數功能和習語。函數式編程技術有明確定義,但語言有時為相同的函數式概念使用不同的術語,使得我們很難看到相似之處。在本期文章中,我比較了 Scala、Groovy 和 Clojure 的函數式編碼風格并討論了它們的優勢。
命令式處理
我要首先探討一個常見問題及其命令式解決方案。假如給定一個名稱列表,其中一些名稱包含一個字符。系統會要求您在一個逗號分隔的字符串中返回名稱,該字符串中不包含單字母的名稱,每個名稱的首字母都大寫。實現該算法的 Java 代碼如清單 1 所示。
清單 1. 命令式處理
public class TheCompanyProcess { public String cleanNames(List<String> listOfNames) { StringBuilder result = new StringBuilder(); for(int i = 0; i < listOfNames.size(); i++) { if (listOfNames.get(i).length() > 1) { result.append(capitalizeString(listOfNames.get(i))).append(","); } } return result.substring(0, result.length() - 1).toString(); } public String capitalizeString(String s) { return s.substring(0, 1).toUpperCase() + s.substring(1, s.length()); } }
由于您必須處理整個列表,解決清單 1 中問題最簡單的方式是使用一個命令式循環。對于每個名稱,都需要進行檢查,確認其長度是否大于 1,然后(如果長度大于 1)將首字母大寫的名稱附加到 result 字符串,并在后面加逗號。最終字符串中的最后一個名稱不應包含逗號,所以我將它從最后返回值中移走。
在命令式編程中,建議您在較低級上別執行操作。在 清單 1 中的 cleanNames() 方法中,我執行了三個任務:我篩選 列表以消除單字符,將列表中每個名稱的首字母變換 為大寫,然后將列表轉化 為一個字符串。在命令式語言中,我不得不為三個任務都使用同一低級機制(對列表進行迭代)。函數式語言將篩選、變換和轉化視為常見操作,因此它們提供給您從不同視角解決問題的方式。
函數式處理
函數編程語言與命令式語言的問題分類方式不同。篩選、變換和轉化邏輯類別表現為函數。那些函數實現低級變換并依賴于開發人員來編寫作為參數傳遞的函數,進而定制函數的行為。我可以用偽代碼將 清單 1 中的問題概念化為:
listOfEmps -> filter(x.length > 1) -> transform(x.capitalize) -> convert(x, y -> x + "," + y)
利用函數式語言,您可以建模這一概念性解決方案,無需擔心實現細節。
Scala 實現
清單 2 使用 Scala 實現 清單 1 中的處理示例。它看起來就像是前面的偽代碼,包含必要的實現細節。
清單 2. Scala 處理
val employees = List("neal", "s", "stu", "j", "rich", "bob") val result = employees .filter(_.length() > 1) .map(_.capitalize) .reduce(_ + "," + _)
對于給定的名稱列表,我首先篩選它,剔除長度不大于 1 的所有名稱。然后將該操作的輸出提供給 map() 函數,該函數對集合的每個元素執行所提供的代碼塊,返回變換后的集合。最后,來自 map() 的輸出集合流向 reduce() 函數,該函數基于代碼塊中提供的規則將每個元素結合起來。
在本例中,我將每對元素結合起來,用插入的逗號連接它們。我不必考慮三個函數調用中參數的名稱是什么,所以我可以使用方便的 Scala 快捷方式,也就是說,使用 _ 跳過名稱。reduce() 函數從前兩個元素入手,將它們結合成一個元素,成為下一個串接中的第一個元素。在 “瀏覽” 列表的同時,reduce() 構建了所需的逗號分隔的字符串。
我首先展示 Scala 實現是因為我對它的語法比較熟悉,而且 Scala 分別為篩選、變換和轉化概念使用了行業通用的名稱,即 filter、map 和 reduce。
Groovy 實現
Groovy 擁有相同的功能,但對它們進行命名的方式與腳本語言(比如 Ruby)更加一致。清單 1 中處理示例的 Groovy 版本如清單 3 所示。
清單 3. Groovy 處理
class TheCompanyProcess { public static String cleanUpNames(List listOfNames) { listOfNames .findAll {it.length() > 1} .collect {it.capitalize()} .join(',') } }
盡管清單 3 在結構上類似于 清單 2 中的 Scala 示例,但方法名稱不同。Groovy 的 findAll 集合方法應用所提供的代碼塊,保留代碼塊為 true 的元素。如同 Scala,Groovy 包含一個隱式參數機制,為單參數代碼塊使用預定義的 it 隱式參數。collect 方法(Groovy 的 map 版本)對集合的每個元素執行所提供的代碼塊。Groovy 提供一個函數 (join()),使用所提供的分隔符將字符串集合串聯為單一字符串,這正是本示例中所需要的。
Clojure 實現
Clojure 是一個使用 reduce、map 和 filter 函數名的函數式語言,如清單 4 所示。
清單 4. Clojure 處理示例
(defn process [list-of-emps] (reduce str (interpose "," (map clojure.string/capitalize (filter #(< 1 (count %)) list-of-emps)))))
Clojure 的 thread-first 宏
thread-last 宏 使集合的處理變得更加簡單。類似的 Clojure 宏 thread-first 可簡化與 Java API 的交互。例如普遍的 Java 代碼語句 person.getInformation().
getAddress().getPostalCode(),這體現了 Java 違反 迪米特法則 的傾向。這種類型的語句給 Clojure 編程帶來一些煩惱,迫使使用 Java API 的開發人員不得不構建由內而外的語句,比如 (getPostalCode (getAddress (getInformation person)))。thread-first 宏消除了這一語法困擾。您可以使用宏將嵌套調用編寫為 (-> person getInformation getAddress getPostalCode),想嵌套多少層都可以。
如果您不習慣查看 Clojure,可以使用清單 4 中的代碼,其結構可能不夠清晰。Clojure 這樣的 Lisp 是 “由內而外” 進行工作的,所以必須從最后的參數值 list-of-emps 著手。Clojure 的 (filter ) 函數接受兩個參數:用于進行篩選的函數(本例中為匿名函數)和要篩選的集合。
您可以為第一個參數編寫一個正式函數定義,比如 (fn [x] (< 1 (count x))),但使用 Clojure 可以更簡潔地編寫匿名函數。與前面的示例一樣,篩選操作的結果是一個較少的集合。(map ) 函數將變換函數接受為第一個參數,將集合(本例中是 (filter ) 操作的返回值)作為第二個參數。Clojure 的 (map ) 函數的第一個參數通常是開發人員提供的函數,但接受單一參數的任何函數都有效;內置 capitalize 函數也符合要求。
最后,(map ) 操作的結果成為了 (reduce ) 的集合參數。(reduce ) 的第一個參數是組合函數(應用于 (interpose ) 的返回的 (str ))。(interpose ) 在集合的每個元素之間(除了最后一個)插入其第一個參數。
當函數嵌套過多時,即使最有經驗的開發人員也會倍感頭疼,如 清單 4 中的 (process ) 函數所示。所幸的是,Clojure 包含的宏支持您將結構 “調整” 為更可讀的順序。清單 5 中的功能與 清單 4 中的功能一樣。
清單 5. 使用 Clojure 的 thread-last 宏
(defn process2 [list-of-emps] (->> list-of-emps (filter #(< 1 (count %))) (map clojure.string/capitalize) (interpose ",") (reduce str)))
Clojure thread-last 宏采取對集合應用各種變換的常見操作并顛倒典型的 Lisp 的順序,恢復了從左到右的更自然的閱讀方式。在 清單 5 中,首先是 (list-of-emps) 集合。代碼塊中每個隨后的表單被應用于前一個表單。Lisp 的優勢之一在于其語法靈活性:任何時候代碼的可讀性變得很差時,您都可以將代碼調整回具有較高可讀性。
函數式編程的優勢
在一篇標題為 “Beating the Averages” 的著名文章中,Paul Graham 定義了 Blub Paradox:他 “編造” 了一種名為 Blub 的虛假語言,并且考慮在其他語言與 Blub 之間進行功能比較:
只要我們假想的 Blub 程序員往下看一連串功能,他就知道自己是在往下看。不如 Blub 功能強大的語言顯然不怎么強大,因為它們缺少程序員習慣使用的一些功能。但當我們假想的 Blub 程序員從另一個方向,也就是說,往上看一連串功能時,他并沒有意識到自己在往上看。他看到的只不過是怪異的語言。他可能認為它們在功能上與 Blub 幾近相同,只是多了其他難以理解的東西。Blub 對他而言已經足夠好,因為他是在 Blub 環境中可以思考問題。
對于很多 Java 開發人員而言,清單 2 中的代碼看起來陌生而又奇怪,因此難以將它看作是有優勢的代碼。但當您停止過于細化任務執行細節時,就釋放了越來越智能的語言和運行時的潛能,從而做出了強大的改進。例如,JVM 的到來(解除了開發人員的內存管理困擾)為先進垃圾回收的創建開辟了全新的研發領域。使用命令式編碼時,您深陷于迭代循環的細節,難以進行并行性等優化。從更高的層面思考操作(比如 filter、map 和 reduce)可將概念與實現分離開來,將并行性等修改從一項復雜、詳細的任務轉變為一個簡單的 API 更改。
想一想如何將 清單 1 中的代碼變為多線程代碼。由于您密切參與了 for 循環期間發生的細節,所以您還必須處理煩人的并發代碼。然后思考一下清單 6 所示的 Scala 并行版本。
清單 6. 實現進程并行性
val parallelResult = employees .par .filter(f => f.length() > 1) .map(f => f.capitalize) .reduce(_ + "," + _)
清單 2 與 清單 6 之間惟一的差別在于,將 .par 方法添加到了命令流中。.par 方法返回后續操作依據的集合的并行版本。由于我將對集合的操作指定為高階概念,所以底層運行時可以自由地完成更多的工作。
面向命令式對象的開發人員往往會考慮使用重用類,因為他們的語言鼓勵將類作為構建塊。函數編程語言傾向于重用函數。函數式語言構建復雜的通用功能(比如 filter()、map() 和 reduce())并通過作為參數提供的函數來實現定制。在函數式語言中,將數據結構轉換為列表和映射等標準集合是很尋常的事,因為它們接著就可以被強大的內置函數所操控。
例如,在 Java 環境中存在許多 XML 處理框架,每個框架都封裝自己的私有版本的 XML 結構,并通過自己的方法交付它。在 Clojure 這樣的語言中,XML 被轉換為基于映射的標準數據結構,該結構對已經存在于語言中的強大的變換、約簡和篩選操作開放。
結束語
所有現代語言都包含或添加了函數式編程結構,使函數式編程成為未來開發中不可或缺的一部分。Java 下一代語言都實現了強大的函數式功能,有時使用不同的名稱和行為。在本期中,我介紹了 Scala、Groovy 和 Clojure 中的一種新編碼風格并展示了一些優勢。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。