functor_纯Java中的Functor和Monad示例
functor
本文最初是我們使用RxJava進行React式編程的附錄。 但是,盡管與React式編程非常相關,但對monad的介紹卻不太適合。 因此,我決定將其取出并作為博客文章單獨發布。 我知道,“ 我對單子的自己的,一半正確和一半的完整解釋 ”是編程博客上的新“ Hello,world ”。 然而,本文從Java數據結構和庫的特定角度研究了函子和monad。 因此,我認為值得分享。
RxJava的設計和構建基于非常基本的概念,例如函子 , monoid和monad 。 盡管Rx最初是為命令式C#語言建模的,并且我們正在學習RxJava,并在類似的命令式語言上工作,但該庫還是源于函數式編程。 在意識到RxJava API的緊湊性之后,您應該不會感到驚訝。 幾乎只有少數幾個核心類,通常是不可變的,并且所有內容都主要由純函數組成。
隨著函數式編程(或函數式樣式)的最新興起(最普遍地用Scala或Clojure等現代語言表示),monads成為了廣泛討論的話題。 他們周圍有很多民間傳說:
monad是endofunctors類別中的monoid,這是什么問題?
詹姆斯·伊里
該monad的詛咒是,一旦您獲得了頓悟,一旦您理解了“哦,就是這樣”,您就失去了向任何人解釋它的能力。
道格拉斯·克羅克福德
絕大多數程序員,尤其是那些沒有函數式編程背景的程序員,都傾向于認為monad是某種神秘的計算機科學概念,因此從理論上講,它對他們的編程事業無濟于事。 這種消極的觀點可以歸因于數十篇文章或博客文章太抽象或太狹窄。 但是事實證明,甚至標準的Java庫都存在monad,特別是自Java Development Kit(JDK)8起(稍后會有更多介紹)。 絕對妙不可言的是,一旦您第一次了解monad,突然之間就會有幾個完全不相同的目的無關的類和抽象變得熟悉。
Monad概括了各種看似獨立的概念,因此學習Monad的另一種化身只需很少的時間。 例如,您不必學習CompletableFuture在Java 8中的工作方式,一旦意識到它是monad,就可以精確地知道它是如何工作的,并且可以從其語義中得到什么。 然后您會聽說RxJava聽起來有很多不同,但是由于Observable是monad,因此沒有太多可添加的。 您已經不知不覺中已經遇到過許多其他的單子示例。 因此,即使您實際上沒有使用RxJava,本節也將是有用的復習。
函子
在解釋什么是monad之前,讓我們研究一個稱為functor的簡單結構。 函子是封裝某些值的類型化數據結構。 從語法的角度來看,函子是具有以下API的容器:
import java.util.function.Function;interface Functor<T> {<R> Functor<R> map(Function<T, R> f);}但是僅僅語法是不足以了解什么是函子。 functor提供的唯一操作是帶函數f map() 。 此函數接收框內的任何內容,對其進行轉換并將結果按原樣包裝到另一個函子中。 請仔細閱讀。 Functor<T>始終是不可變的容器,因此map不會使執行該操作的原始對象發生突變。 取而代之的是,它返回包裝在全新函子中的結果(或結果–請耐心等待),該函子可能是類型R 此外,在應用標識函數(即map(x -> x)時,函子不應執行任何操作。 這種模式應始終返回相同的函子或相等的實例。
通常將Functor<T>與保存T實例進行比較,其中與該值交互的唯一方法是對其進行轉換。 但是,沒有從函子解開或逃逸的慣用方式。 值始終在函子的上下文內。 函子為什么有用? 它們使用一個統一的,適用于所有對象的統一API來概括多個通用習語,如集合,promise,Optionals等。 讓我介紹幾個函子,以使您更流暢地使用此API:
interface Functor<T,F extends Functor<?,?>> {<R> F map(Function<T,R> f); }class Identity<T> implements Functor<T,Identity<?>> {private final T value;Identity(T value) { this.value = value; }public <R> Identity<R> map(Function<T,R> f) {final R result = f.apply(value);return new Identity<>(result);}}需要額外的F類型參數來進行Identity編譯。 在前面的示例中,您看到的是最簡單的函子,僅包含一個值。 您只能使用map方法內部的值對其進行轉換,但無法提取它。 這被認為超出了純函子的范圍。 與函子進行交互的唯一方法是應用類型安全的轉換序列:
Identity<String> idString = new Identity<>("abc"); Identity<Integer> idInt = idString.map(String::length);或流利地,就像您編寫函數一樣:
Identity<byte[]> idBytes = new Identity<>(customer).map(Customer::getAddress).map(Address::street).map((String s) -> s.substring(0, 3)).map(String::toLowerCase).map(String::getBytes);從這個角度來看,在函子上的映射與調用鏈式函數沒有太大不同:
byte[] bytes = customer.getAddress().street().substring(0, 3).toLowerCase().getBytes();您為什么還要煩惱這種冗長的包裝,不僅不提供任何附加值,而且也無法將內容提取回去? 好吧,事實證明您可以使用此原始函子抽象對其他幾個概念建模。 例如,從Java 8開始的java.util.Optional<T>是帶有map()方法的函子。 讓我們從頭開始實現它:
class FOptional<T> implements Functor<T,FOptional<?>> {private final T valueOrNull;private FOptional(T valueOrNull) {this.valueOrNull = valueOrNull;}public <R> FOptional<R> map(Function<T,R> f) {if (valueOrNull == null)return empty();elsereturn of(f.apply(valueOrNull));}public static <T> FOptional<T> of(T a) {return new FOptional<T>(a);}public static <T> FOptional<T> empty() {return new FOptional<T>(null);}}現在變得有趣了。 FOptional<T>函子可以保存一個值,但也可以為空。 這是一種對null進行編碼的類型安全的方法。 構造FOptional方法有兩種:通過提供值或創建empty()實例。 在這兩種情況下,就像Identity , FOptional是不可變的,我們只能從內部與值交互。 FOptional不同之FOptional在于,如果轉換函數f為空,則它可能不會應用于任何值。 這意味著函子可能不必完全封裝類型T一個值。 它也可以包裝任意數量的值,就像List …functor:
import com.google.common.collect.ImmutableList;class FList<T> implements Functor<T, FList<?>> {private final ImmutableList<T> list;FList(Iterable<T> value) {this.list = ImmutableList.copyOf(value);}@Overridepublic <R> FList<?> map(Function<T, R> f) {ArrayList<R> result = new ArrayList<R>(list.size());for (T t : list) {result.add(f.apply(t));}return new FList<>(result);} }API保持不變:在轉換T -> R使用函子,但是行為卻大不相同。 現在,我們對FList每個項目應用轉換,以聲明方式轉換整個列表。 因此,如果您有一個customers列表,并且想要他們的街道列表,則非常簡單:
import static java.util.Arrays.asList;FList<Customer> customers = new FList<>(asList(cust1, cust2));FList<String> streets = customers.map(Customer::getAddress).map(Address::street);這不再像說customers.getAddress().street()那樣簡單,您不能在一組客戶上調用getAddress() ,必須在每個單獨的客戶上調用getAddress() ,然后將其放回一個集合中。 順便說一句,Groovy發現這種模式是如此普遍,以至于實際上有一個語法糖: customer*.getAddress()*.street() 。 該運算符稱為散點圖,實際上是變相的map 。 也許您想知道為什么我要在map list手動遍歷list而不是使用Java 8中的Stream : list.stream().map(f).collect(toList()) ? 這會響嗎? 如果我告訴您Java java.util.stream.Stream<T>也是一個函子怎么辦? 順便說一句,一個單子?
現在,您應該看到函子的第一個好處–它們抽象了內部表示形式,并為各種數據結構提供了一致且易于使用的API。 作為最后一個示例,讓我介紹類似于Future promise函數。 Promise “承諾”某一天將提供一個值。 它尚未出現,可能是因為產生了一些后臺計算,或者我們正在等待外部事件。 但是它將在將來出現。 完成Promise<T>的機制并不有趣,但是函子的性質是:
Promise<Customer> customer = //... Promise<byte[]> bytes = customer.map(Customer::getAddress).map(Address::street).map((String s) -> s.substring(0, 3)).map(String::toLowerCase).map(String::getBytes);看起來很熟悉? 這就是重點! Promise函子的實現超出了本文的范圍,甚至不重要。 不用說,我們非常接近從Java 8實現CompletableFuture ,并且幾乎從RxJava中發現了Observable 。 但是回到函子。 Promise<Customer>尚未持有Customer的值。 它有望在將來具有這種價值。 但是我們仍然可以像使用FOptional和FList一樣映射此類函子–語法和語義完全相同。 行為遵循函子表示的內容。 調用customer.map(Customer::getAddress)產生Promise<Address> ,這意味著map是非阻塞的。 customer.map() 不會等待基礎的customer承諾完成。 相反,它返回另一個不同類型的承諾。 當上游承諾完成時,下游承諾將應用傳遞給map()的函數并將結果傳遞給下游。 突然,我們的函子使我們能夠以非阻塞方式流水線進行異步計算。 但是您不必理解或學習-因為Promise是函子,所以它必須遵循語法和法則。
函子還有許多其他很好的例子,例如以組合方式表示值或錯誤。 但是現在是時候看看單子了。
從函子到單子
我假設您了解函子是如何工作的,為什么它們是有用的抽象。 但是函子并不像人們期望的那樣普遍。 如果您的轉換函數(作為參數傳遞給map()那個)返回函子實例而不是簡單值,會發生什么? 好吧,函子也只是一個值,所以沒有壞事發生。 將返回的所有內容放回函子中,以便所有行為都保持一致。 但是,假設您有以下方便的方法來解析String :
FOptional<Integer> tryParse(String s) {try {final int i = Integer.parseInt(s);return FOptional.of(i);} catch (NumberFormatException e) {return FOptional.empty();} }例外是會破壞類型系統和功能純度的副作用。 在純函數語言中,沒有例外的地方,畢竟我們從來沒有聽說過在數學課上拋出例外,對嗎? 錯誤和非法條件使用值和包裝器明確表示。 例如, tryParse()接受一個String但并不簡單地返回int或在運行時靜默引發異常。 通過類型系統,我們明確告知tryParse()可能失敗,字符串格式錯誤不會有任何異常或錯誤。 此半故障由可選結果表示。 有趣的是,Java已經檢查了必須聲明和處理的異常,因此從某種意義上講,Java在這方面比較純凈,它沒有隱藏副作用。 但是,無論好壞,通常在Java中不建議使用檢查異常,因此讓我們回到tryParse() 。 用已經包裝在FOptional String組成tryParse似乎很有用:
FOptional<String> str = FOptional.of("42"); FOptional<FOptional<Integer>> num = str.map(this::tryParse);這不足為奇。 如果tryParse()返回一個int您將得到FOptional<Integer> num ,但是由于map()函數返回FOptional<Integer>本身,因此它被包裝兩次,成為笨拙的FOptional<FOptional<Integer>> 。 請仔細查看類型,您必須了解為什么在這里獲得此雙重包裝。 除了看上去很恐怖之外,在函子中放一個函子會破壞構圖和流暢的鏈接:
FOptional<Integer> num1 = //... FOptional<FOptional<Integer>> num2 = //...FOptional<Date> date1 = num1.map(t -> new Date(t));//doesn't compile! FOptional<Date> date2 = num2.map(t -> new Date(t));在這里,我們嘗試通過將int轉換為+ Date +來映射FOptional的內容。 具有Functor<Integer> int -> Date的功能,我們可以輕松地從Functor<Integer>為Functor<Date> ,我們知道它是如何工作的。 但是在num2情況下情況變得復雜。 num2.map()接收的輸入不再是int而是FOoption<Integer> ,顯然java.util.Date沒有這樣的構造函數。 我們通過雙重包裹來破壞了函子。 但是,具有返回函子而不是簡單值的函數非常普遍(例如tryParse() ),因此我們不能簡單地忽略這種要求。 一種方法是引入一種特殊的無參數join()方法,以“展平”嵌套函子:
FOptional<Integer> num3 = num2.join()它可以工作,但是因為這種模式太普遍了,所以引入了一種名為flatMap()特殊方法。 flatMap()與map非常相似,但希望作為參數接收的函數返回函子-或monad是精確的:
interface Monad<T,M extends Monad<?,?>> extends Functor<T,M> {M flatMap(Function<T,M> f); }我們僅得出結論, flatMap只是一種語法糖,可以實現更好的組合。 但是flatMap方法(通常稱為Haskell的bind或>>= )具有所有不同之處,因為它允許以純函數式的形式構成復雜的轉換。 如果FOptional是monad的實例,則解析突然可以按預期進行:
FOptional<Integer> num = FOptional.of(42); FOptional<Integer> answer = num.flatMap(this::tryParse);Monads不需要實現map ,可以輕松地在flatMap()之上實現它。 實際上, flatMap是啟用全新轉換領域的基本運算符。 顯然,就像函子一樣,語法順從性還不足以將某個類稱為monad, flatMap()運算符必須遵循monad定律,但它們非常直觀,就像flatMap()與標識的關聯性一樣。 后者要求m(x).flatMap(f)與持有值x任何monad和函數f f(x)相同。 我們不會深入研究monad理論,而讓我們關注實際含義。 當內部結構并非無關緊要時,Monad便會發光,例如Promise monad,它將在將來具有一定的價值。 您可以從類型系統中猜測Promise在以下程序中的表現嗎? 首先,所有可能花費一些時間才能完成的方法都會返回Promise :
import java.time.DayOfWeek;Promise<Customer> loadCustomer(int id) {//... }Promise<Basket> readBasket(Customer customer) {//... }Promise<BigDecimal> calculateDiscount(Basket basket, DayOfWeek dow) {//... }現在,我們可以將這些函數組合起來,就好像它們都是使用單子運算符進行了阻塞一樣:
Promise<BigDecimal> discount = loadCustomer(42).flatMap(this::readBasket).flatMap(b -> calculateDiscount(b, DayOfWeek.FRIDAY));這變得很有趣。 flatMap()必須保留monadic類型,因為所有中間對象都是Promise 。 不僅僅是保持類型有序-先前的程序突然完全異步! loadCustomer()返回Promise因此不會阻塞。 readBasket()接受Promise擁有(將要擁有的一切)并應用返回另一個Promise的函數,依此類推。 基本上,我們建立了一個異步計算管道,其中后臺完成一個步驟會自動觸發下一步。
探索
有兩個單子并將它們包含的值組合在一起是很常見的。 但是函子和monad均不允許直接訪問其內部,這是不純的。 相反,我們必須謹慎地應用轉換,而不能逃脫monad。 假設您有兩個單子,并且您想將它們合并
import java.time.LocalDate; import java.time.Month;Monad<Month> month = //... Monad<Integer> dayOfMonth = //...Monad<LocalDate> date = month.flatMap((Month m) ->dayOfMonth.map((int d) -> LocalDate.of(2016, m, d)));請花點時間研究前面的偽代碼。 我沒有使用任何真正的monad實現(例如Promise或List來強調核心概念。 我們有兩個獨立的monad,一個是Month類型,另一個是Integer類型。 為了從中構建LocalDate ,我們必須構建一個嵌套的轉換,該轉換可以訪問兩個monad的內部。 仔細研究這些類型,尤其要確保您了解為什么我們在一個地方使用flatMap在另一個地方使用map() 。 想想如果您還有第三個Monad<Year> ,那么您將如何構造此代碼。 這種應用兩個參數(在本例中為m和d )的函數的模式非常普遍,以至于Haskell中有一個特殊的輔助函數,稱為liftM2 ,它完全在map和flatMap上實現了這種轉換。 在Java偽語法中,它看起來像這樣:
Monad<R> liftM2(Monad<T1> t1, Monad<T2> t2, BiFunction<T1, T2, R> fun) {return t1.flatMap((T1 tv1) ->t2.map((T2 tv2) -> fun.apply(tv1, tv2))); }您不必為每個monad實現此方法, flatMap()足夠了,而且它對所有monad都一致地起作用。 當您考慮如何將其與各種monad一起使用時, liftM2非常有用。 例如listM2(list1, list2, function)將對list1和list2 (笛卡爾積)中的每對可能的項應用function 。 另一方面,對于可選選項,僅當兩個可選選項均為非空時,它將應用功能。 更好的是,對于Promise monad,當兩個Promise都完成時,將異步執行一個函數。 這意味著我們只是發明了一個簡單的同步機制(分叉聯接算法中的join() ,該同步機制包含兩個異步步驟。
我們可以輕松地在flatMap()之上構建的另一個有用的運算符是filter(Predicate<T>) ,該運算符接收monad內部的所有內容,如果不符合某些謂詞,則將其完全丟棄。 在某種程度上,它類似于map但不是1-to-1映射,而是1-to-0-or-1。 同樣, filter()對于每個monad具有相同的語義,但是取決于我們實際使用的monad,其功能相當驚人。 顯然,它允許從列表中過濾掉某些元素:
FList<Customer> vips = customers.filter(c -> c.totalOrders > 1_000);但是它也可以正常工作,例如對于可選項目。 在這種情況下,如果可選內容不符合某些條件,我們可以將非空可選內容轉換為空值。 空的可選部分保持不變。
從單子列表到單子列表
另一個來自flatMap()有用運算符是sequence() 。 您只需查看類型簽名即可輕松猜測其作用:
Monad<Iterable<T>> sequence(Iterable<Monad<T>> moands)通常,我們有一堆相同類型的monad,而我們想要一個具有該類型列表的monad。 對您來說,這聽起來似乎很抽象,但卻非常有用。 想象一下,您想通過ID同時從數據庫中加載一些客戶,因此您多次對不同的ID使用loadCustomer(id)方法,每次調用都返回Promise<Customer> 。 現在,您有了Promise的列表,但您真正想要的是客戶列表,例如要在Web瀏覽器中顯示的客戶列表。 sequence() (在RxJava中sequence()稱為concat()或merge() ,具體取決于用例)是為此目的而構建的:
FList<Promise<Customer>> custPromises = FList.of(1, 2, 3).map(database::loadCustomer);Promise<FList<Customer>> customers = custPromises.sequence();customers.map((FList<Customer> c) -> ...);通過為每個ID調用database.loadCustomer(id) ,我們可以在其上map一個表示客戶ID的FList<Integer> (您看到FList是一個函子嗎?) 這導致Promise列表非常不便。 sequence()節省了一天的時間,但是再次這不僅是語法糖。 前面的代碼是完全非阻塞的。 對于不同種類的monads, sequence()仍然有意義,但是在不同的計算上下文中。 例如,可以將FList<FOptional<T>>更改為FOptional<FList<T>> 。 順便說一句,您可以在flatMap()之上實現sequence() (就像map()一樣flatMap() 。
一般而言,這只是關于flatMap()和monad有用性的冰山一角。 盡管源于晦澀的類別理論,但即使在Java之類的面向對象的編程語言中,monad也被證明是極其有用的抽象。 能夠組成返回單子函數的函數非常有用,以至于數十個無關的類遵循單子行為。
而且,一旦將數據封裝在monad中,通常很難顯式地將其取出。 這種操作不是monad行為的一部分,并且經常導致非慣用語代碼。 例如, Promise<T>上的Promise.get()可以從技術上返回T ,但是只能通過阻塞來返回,而所有基于flatMap()運算符都是非阻塞的。 另一個示例是FOptional.get()可能會失敗,因為FOptional可能為空。 即使FList.get(idx)從列表偷窺特定元素聽起來很別扭,因為你可以替換for與循環map()經常。
我希望您現在了解為什么單子如此流行。 即使在像Java這樣的面向對象的語言中,它們也是非常有用的抽象。
翻譯自: https://www.javacodegeeks.com/2016/06/functor-monad-examples-plain-java.html
functor
總結
以上是生活随笔為你收集整理的functor_纯Java中的Functor和Monad示例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 福建房产备案信息查询(福建房产备案)
- 下一篇: spring 自定义日志_Spring和