java 函数式编程 示例_功能Java示例 第8部分–更多纯函数
java 函數式編程 示例
這是第8部分,該系列的最后一部分稱為“示例功能Java”。
我在本系列的每個部分中開發的示例是某種“提要處理程序”,用于處理文檔。 在上一期文章中,我們已經使用Vavr庫看到了一些模式匹配,并且還將故障也視為數據 ,例如,采用了替代路徑并返回到功能流程。
在本系列的最后一篇文章中,我將功能發揮到了極致 :一切都變成了功能。
如果您是第一次來,最好是從頭開始閱讀。 它有助于了解我們從何處開始以及如何在整個系列中繼續前進。
這些都是這些部分:
- 第1部分–從命令式到聲明式
- 第2部分–講故事
- 第3部分–不要使用異常來控制流程
- 第4部分–首選不變性
- 第5部分–將I / O移到外部
- 第6部分–用作參數
- 第7部分–將失敗也視為數據
- 第8部分–更多純函數
我將在每篇文章發表時更新鏈接。 如果您通過內容聯合組織來閱讀本文,請查看我博客上的原始文章。
每次代碼也被推送到這個GitHub項目 。
最大化運動部件
您可能已經聽過Micheal Feathers的以下短語:
OO通過封裝運動部件使代碼易于理解。 FP通過最大程度地減少運動部件來使代碼易于理解。
好的,讓我們稍稍忘記上一期中的故障恢復,然后繼續下面的版本:
FeedHandler { class FeedHandler { List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator) { changes .findAll { doc -> isImportant(doc) } .collect { doc -> creator.apply(doc) }.map { resource -> setToProcessed(doc, resource) }.getOrElseGet { e -> setToFailed(doc, e) } } } private static boolean isImportant(doc) { doc.type == 'important' } private static Doc setToProcessed(doc, resource) { doc.copyWith( status: 'processed' , apiId: resource.id ) } private static Doc setToFailed(doc, e) { doc.copyWith( status: 'failed' , error: e.message ) } }替換為功能類型
我們可以使用對函數接口類型的變量(例如Predicate或BiFunction的引用來替換每種方法。
A)我們可以替換一個接受1個參數的方法,該方法返回一個布爾值 。
private static boolean isImportant(doc) { doc.type == 'important' }由謂詞
private static Predicate<Doc> isImportant = { doc -> doc.type == 'important' }B),我們可以替換一個接受2個參數并返回結果的方法
private static Doc setToProcessed(doc, resource) { ... } private static Doc setToFailed(doc, e) { ... }具有BiFunction
private static BiFunction<Doc, Resource, Doc> setToProcessed = { doc, resource -> ... } private static BiFunction<Doc, Throwable, Doc> setToFailed = { doc, e -> ... }為了實際調用封裝在(Bi)Function中的邏輯,我們必須對其調用apply 。 結果如下:
FeedHandler { class FeedHandler { List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator) { changes .findAll { isImportant } .collect { doc -> creator.apply(doc) .map { resource -> setToProcessed.apply(doc, resource) }.getOrElseGet { e -> setToFailed.apply(doc, e) } } } private static Predicate<Doc> isImportant = { doc -> doc.type == 'important' } private static BiFunction<Doc, Resource, Doc> setToProcessed = { doc, resource -> doc.copyWith( status: 'processed' , apiId: resource.id ) } private static BiFunction<Doc, Throwable, Doc> setToFailed = { doc, e -> doc.copyWith( status: 'failed' , error: e.message ) } }將所有輸入移至功能本身
我們將所有內容移至方法簽名,以便FeedHandler的handle方法的調用者可以提供自己的那些功能的實現。
方法簽名將更改為:
List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator)至
List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator, Predicate<Doc> filter, BiFunction<Doc, Resource, Doc> successMapper, BiFunction<Doc, Throwable, Doc> failureMapper)其次,我們將重命名原始(靜態) 謂詞和BiFunction變量
- isImportant
- setToProcessed
- setToFailed
轉換為類頂部的新常量 ,反映它們的新作用。
- DEFAULT_FILTER
- DEFAULT_SUCCESS_MAPPER
- DEFAULT_FAILURE_MAPPER
客戶端可以完全控制是否將默認實現用于某些功能,或者何時需要接管自定義邏輯。
例如,當僅需要定制故障處理時,可以這樣調用handle方法:
BiFunction<Doc, Throwable, Doc> customFailureMapper = { doc, e -> doc.copyWith( status: 'my-custom-fail-status' , error: e.message ) } new FeedHandler().handle(..., FeedHandler.DEFAULT_FILTER, FeedHandler.DEFAULT_SUCCESS_MAPPER, customFailureMapper )如果您的語言支持,則可以通過分配默認值來確??蛻舳藢嶋H上不必提供每個參數。 我正在使用支持將默認值分配給方法中的參數的Apache Groovy :
List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator, Predicate<Doc> filter = DEFAULT_FILTER, BiFunction<Doc, Resource, Doc> successMapper = DEFAULT_SUCCESS_MAPPER, BiFunction<Doc, Throwable, Doc> failureMapper = DEFAULT_FAILURE_MAPPER)在我們將應用另一個更改之前,請看一下代碼:
FeedHandler { class FeedHandler { private static final Predicate<Doc> DEFAULT_FILTER = { doc -> doc.type == 'important' } private static final BiFunction<Doc, Resource, Doc> DEFAULT_SUCCESS_MAPPER = { doc, resource -> doc.copyWith( status: 'processed' , apiId: resource.id ) } private static final BiFunction<Doc, Throwable, Doc> DEFAULT_FAILURE_MAPPER = { doc, e -> doc.copyWith( status: 'failed' , error: e.message ) } List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator, Predicate<Doc> filter = DEFAULT_FILTER, BiFunction<Doc, Resource, Doc> successMapper = DEFAULT_SUCCESS_MAPPER, BiFunction<Doc, Throwable, Doc> failureMapper = DEFAULT_FAILURE_MAPPER) { changes .findAll { filter } .collect { doc -> creator.apply(doc) .map { resource -> successMapper.apply(doc, resource) }.getOrElseGet { e -> failureMapper.apply(doc, e) } } } }介紹兩者
您是否注意到以下部分?
.collect { doc -> creator.apply(doc) .map { resource -> successMapper.apply(doc, resource) }.getOrElseGet { e -> failureMapper.apply(doc, e) } }請記住, creator的類型是
Function<Doc, Try<Resource>>表示它返回一個Try 。 我們在第7部分中介紹了Try ,它是從Scala等語言中借來的。
幸運的是, collect { doc的“ doc”變量仍在傳遞給我們需要它的successMapper和failureMapper 范圍內 ,但是Try#map的方法簽名(接受一個Function )與我們的successMapper (即一個BiFunction 。 Try#getOrElseGet也是Try#getOrElseGet ,它也只需要一個Function 。
從Try Javadocs:
- map(Function <?super T,?extended U>映射器)
- getOrElseGet(Function <?super Throwable,?extended T> other)
簡而言之,我們需要從
至
同時仍然可以將原始文檔作為輸入 。
讓我們介紹兩個簡單的類型,它們封裝了2個BiFunction的2個參數:
class CreationSuccess { Doc doc Resource resource } class CreationFailed { Doc doc Exception e }我們將論點從
改為功能 :
現在, handle方法如下所示:
List<Doc> handle(List<Doc> changes, Function<Doc, Try<Resource>> creator, Predicate<Doc> filter, Function<CreationSuccess, Doc> successMapper, Function<CreationFailed, Doc> failureMapper) { changes .findAll { filter } .collect { doc -> creator.apply(doc) .map(successMapper) .getOrElseGet(failureMapper) } }…… 但是還不行 。
Try使map和getOrElseGet需要分別。 一個
- 函數<資源,文檔> successMapper
- 函數<Throwable,Doc> failureMapper
這就是為什么我們需要將其更改為另一個著名的FP結構,稱為Either 。
幸運的是Vavr有要么太。 它的Javadoc說:
任一代表兩種可能的值。
通常使用這兩種類型來區分正確的值(“正確”)或錯誤的值。
它變得非常抽象:
一個Either可以是Either.Left或Either.Right。 如果給定的Either是Right并投影到Left,則Left操作對Right值沒有影響。 如果給定的Either是Left并投影到Right,則Right操作對Left值沒有影響。 如果將“左”投影到“左”或將“右”投影到“右”,則操作會生效。
讓我解釋以上神秘的文檔。 如果我們更換
Function<Doc, Try<Resource>> creator通過
Function<Doc, Either<CreationFailed, CreationSuccess>> creator我們將CreationFailed分配給“ left”參數,按照慣例通常會保留錯誤(請參見Either上的Haskell文檔 ), CreationSuccess是“ right”(和“正確”)值。
在運行時,該實現曾經返回Try ,但是現在可以返回Either.Right ,如果成功,例如
return Either.right( new CreationSuccess( doc: document, resource: [id: '7' ] ) )或Either.Left ,但發生故障時除外- 兩者都包括原始文檔 。 是。
因為現在類型最終匹配,所以我們終于壓扁了
.collect { doc -> creator.apply(doc) .map { resource -> successMapper.apply(doc, resource) }.getOrElseGet { e -> failureMapper.apply(doc, e) } }進入
.collect { doc -> creator.apply(doc) .map(successMapper) .getOrElseGet(failureMapper) }現在, handle方法如下所示:
List<Doc> handle(List<Doc> changes, Function<Doc, Either<CreationFailed, CreationSuccess>> creator, Predicate<Doc> filter, Function<CreationSuccess, Doc> successMapper, Function<CreationFailed, Doc> failureMapper) { changes .findAll { filter } .collect { doc -> creator.apply(doc) .map(successMapper) .getOrElseGet(failureMapper) } }結論
我可以說我已經實現了開始時設定的大多數目標:
- 是的,我設法避免了重新分配變量
- 是的,我設法避免了可變數據結構
- 是的,我設法避免了狀態 (至少在FeedHandler中)
- 是的,我設法支持函數 (使用某些Java內置函數類型和某些第三方庫Vavr)
我們已經將所有內容移至函數簽名,以便FeedHandler的handle方法的調用者可以直接傳遞正確的實現。 如果您從頭到尾回顧原始版本,您會注意到在處理更改列表時,我們仍然承擔所有責任:
- 通過某些條件過濾文檔列表
- 為每個文檔創建資源
- 成功創建資源后執行一些操作
- 無法創建資源時執行其他操作
然而,在第一部分中,這些責任是勢在必行寫出來,for語句聲明,都在一個大聚集在一起handle方法。 現在,最后,每個決定或動作都由具有抽象名稱的函數表示,例如“過濾器”,“創建者”,“ successMapper”和“ failureMapper”。 實際上,它成為一個高階函數,以多個函數之一作為參數。 提供所有參數的責任已經轉移到了客戶的上層。 如果您查看GitHub項目,您會注意到,對于這些示例,我不得不不斷更新單元測試。
有爭議的部分
在實踐中,如果不需要,我可能不會編寫我的(Java)業務代碼,例如FeedHandler類在傳遞通用Java函數類型(即Function , BiFunction , Predicate , Consumer , Supplier )方面的使用方式所有這些極端的靈活性。 所有這些都是以可讀性為代價的。 是的,Java是一種靜態類型的語言,因此,使用泛型時,必須在所有類型參數中明確使用一種語言,從而導致以下功能的簽名困難:
handle(List<Doc> changes, Function<Doc, Either<CreationFailed, CreationSuccess>> creator, Predicate<Doc> filter, Function<CreationSuccess, Doc> successMapper, Function<CreationFailed, Doc> failureMapper)在普通JavaScript中,您將沒有任何類型,并且您必須閱讀文檔以了解每個參數的期望。
handle = function (changes, creator, filter, successMapper, failureMapper)但是,這是一個折衷方案。 Groovy中,也是一個JVM語言, 可以讓我省略所有的例子類型的信息在這個系列中,甚至允許我使用閉包(象Java lambda表達式)是在Groovy中的函數式編程范式的核心。
更極端的做法是在類級別指定所有類型,以使客戶端具有最大的靈活性,以便為不同的FeedHandler實例指定不同的類型。
handle(List<T> changes, Function<T, Either<R, S>> creator, Predicate<T> filter, Function<S, T> successMapper, Function<R, T> failureMapper)什么時候合適?
- 如果您完全控制代碼,則在特定上下文中使用它來解決特定問題時,這將過于抽象而無法產生任何收益。
- 但是,如果我將一個庫或框架開源(或者在一個組織內向其他團隊或部門使用),該庫或框架正在各種不同的用例中使用,那么我可能不會事先想到,為靈活性而設計可能值得。 讓呼叫者決定如何過濾以及成功或失敗的構成是明智之舉。
最終,上述內容在API設計 ,是和解耦方面都有所涉及,但是在典型的Enterprise Java Java項目中“使一切成為函數”可能需要與您和您的團隊成員進行一些討論。 多年來,一些同事已經習慣了一種更傳統,更慣用的代碼編寫方式。
好的零件
- 我絕對希望使用不可變的數據結構 (和“參照透明性”)來幫助推斷我的數據所處的狀態。想想Collections.unmodifiableCollection的集合。 在我的示例中,我將Groovy的@Immutable用于POJO,但在普通的Java庫(例如Immutables , AutoValue或Project Lombok)中也可以使用。
- 最大的改進實際上是導致了一種更具功能性的樣式:使代碼講故事 ,這主要是關于分離關注點并適當地命名事物。 在任何編程風格(即使是OO:D)中,這都是一個好習慣,但這確實消除了混亂,并允許引入(純)函數。
- 在Java中,我們習慣于以特定方式進行異常處理,以至于像我這樣的開發人員很難提出其他解決方案。 諸如Haskell之類的功能語言僅返回錯誤代碼,因為“ Niklaus Wirth認為異常是GOTO的轉世,因此省略了它們” 。 在Java中,可以使用CompletableFuture或…
- 通過引入第3方庫(例如Vavr)可在您自己的代碼庫中使用的特定類型(例如Try和Either )可以極大地幫助您啟用以FP樣式編寫的更多選項 ! 我以流暢的方式編寫“成功”或“失敗”路徑并具有很高的可讀性而感到非常著迷。
Java不是F#的Scala或Haskell或Clojure,它最初遵循的是面向對象編程(OOP)范例,就像C ++,C#,Ruby等一樣,但是在Java 8中引入了lambda表達式并結合了一些很棒的功能之后如今,開放源代碼庫如今,開發人員絕對可以選擇OOP和FP必須提供的最佳元素 。
做系列的經驗教訓
我在很早以前就開始了這個系列的討論 。 早在2017年,我發現自己在一段代碼上進行了一些FP風格的重構,這啟發了我去尋找一系列名為“ Functional Java by Example”的文章的示例 。 這成為我在每個批次中一直使用的FeedHandler代碼。
那時我已經對所有的代碼進行了更改,但是當我計劃編寫實際的博客文章時,我常常想到:“我只是不能展示重構,我必須進行實際解釋!” 那就是我為自己設置陷阱的地方,因為在整個過程中,我坐下來寫作的時間越來越少。 (任何寫過博客的人都知道,簡單地分享要點和撰寫可理解的英語co的連貫段落在時間上的區別)
下次當我想到進行一系列學習時,我將向Google返回這些經驗教訓:
- 《功能性思維:語法驚人的范式 》,尼爾·福特(Neil Ford)著,它展示了FP思維的新方法,并且也以不同的方式處理問題。
- 40分鐘內的函數式編程 Russ Olsen的Youtube視頻解釋說:“這些數學家用379頁證明1 + 1 = 2。 讓我們看看我們可以從中吸取什么好主意。”
- 為什么不對函數進行規范編程? 理查德·費爾德曼(Richard Feldman)的Youtube視頻,他解釋了為什么OOP變得非常流行,以及FP為何不是常態。 正如您所知,他是Elm核心團隊的成員,與FP有一定的聯系。
- (耦合)控制的倒置有關“托管功能”的深思熟慮的文章。 您想要抽象嗎?
如果您有任何意見或建議,我很想聽聽他們的意見!
編程愉快! 🙂
翻譯自: https://www.javacodegeeks.com/2019/12/functional-java-by-example-part-8-more-pure-functions.html
java 函數式編程 示例
總結
以上是生活随笔為你收集整理的java 函数式编程 示例_功能Java示例 第8部分–更多纯函数的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 嗦嘎是什么意思 嗦嘎的意思
- 下一篇: java程序连接kafka_Java的K