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