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