絕大多數程式設計師,特別是那些沒有功能編程背景的程式設計師,都傾向於認為monad是某種神秘的計算機科學概念,因此從理論上講,它對他們的編程事業沒有幫助。這種消極的觀點可以歸因於數十篇文章或博客文章過於抽象或過於狹窄。但是事實證明,即使在標準Java庫中,monad也無處不在,尤其是從Java Development Kit(JDK)8開始(以後會有更多介紹)。絕對妙不可言的是,一旦您第一次了解monad,突然之間就會有幾個完全不相同的目的無關的類和抽象變得熟悉。
Monad概括了各種看似獨立的概念,因此學習Monad的另一種化身只需很少的時間。例如,您不必學習CompletableFuture在Java 8中的工作方式-一旦意識到它是monad,就可以精確地知道它的工作方式,以及從其語義中可以期待什麼。然後您會聽說RxJava聽起來有很大不同,但是由於Observable是monad,因此沒有太多要添加的內容。您已經不知不覺中已經遇到過許多其他的Monads示例。因此,即使您實際上沒有使用RxJava,本節也將是有用的複習。
Functors
在解釋什麼是monad之前,讓我們研究一個稱為functor的簡單結構。Functors是封裝某些值的類型化數據結構。從語法的角度來看,Functors是具有以下API的容器:
import java.util.function.Function;
interface Functor<T> {
<R> Functor<R> map(Function<T, R> f);
}
但是僅語法是不足以了解什麼是Functors。functor提供的唯一操作是帶有函數f的map()。此函數接收框內的任何內容,對其進行轉換並將結果按原樣包裝到另一個Functors中。請仔細閱讀。Functor <T>始終是一個不可變的容器,因此map不會使執行該操作的原始對象發生突變。相反,它將返回包裝在全新Functors中的結果(或結果-請耐心等待),Functors可能是類型R。另外,Functors在應用標識函數(即map(x-> x))時不應執行任何操作。這種模式應始終返回相同的Functors或相等的實例。
通常將Functor <T>與保存T的實例進行比較,其中與該值交互的唯一方法是對其進行轉換。但是,沒有從Functors解開或逃逸的慣用方法。值始終位於Functors的上下文內。Functors為什麼有用?它們使用一個統一的,適用於所有集合的API概括了集合,promise,Optionals等多個常見習語。讓我介紹幾個Functors,以使您更流暢地使用此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編譯。在前面的示例中,您看到的是最簡單的Functors,僅包含一個值。您只能在map方法內部對其進行轉換,但是無法提取它。這被認為超出了純Functors的範圍。與Functors進行交互的唯一方法是應用類型安全的轉換序列:
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);
從這個角度來看,在Functors上的映射與調用鏈式函數沒有太大不同:
byte[] bytes = customer
.getAddress()
.street()
.substring(0, 3)
.toLowerCase()
.getBytes();
您為什麼還要打擾這樣冗長的包裝,不僅不提供任何附加值,而且也不能將內容提取回去?好了,事實證明您可以使用此原始Functors抽象對其他幾個概念建模。例如,從Java 8開始,可選的是帶有map()方法的Functors。讓我們從頭開始實現它:
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();
else
return 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()實例。在這兩種情況下,就像with一樣Identity,FOptional都是不可變的,我們只能與內部的值進行交互。不同之處FOptional在於,如果轉換函數f為空,則可能不會將其應用於任何值。這意味著Functors可能未必必須完全封裝type的一個值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);
}
@Override
public <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保持不變:您可以在轉換中使用Functors-但行為卻大不相同。現在,我們對FList中的每個項目進行轉換,以聲明方式轉換整個列表。因此,如果您有客戶列表,並且想要他們的街道列表,則非常簡單:
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偽裝。也許您想知道為什麼我要在list內部手動迭代map而不是使用StreamJava 8中的s list.stream().map(f).collect(toList())?這會響嗎?如果我java.util.stream.Stream<T>用Java 告訴您也是Functors怎麼辦?順便說一句,一個Monads?
現在,您應該看到Functors的第一個好處-它們抽象了內部表示形式,並為各種數據結構提供了一致且易於使用的API。作為最後一個示例,讓我介紹類似於的 promise函數Future。Promise「承諾」有一天將提供一個值。它尚未出現,可能是因為產生了一些後臺計算,或者我們正在等待外部事件。但是它將在將來的某個時間出現。完成a Promise<T>的機制並不有趣,但是Functors的性質是:
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);
看起來很熟悉?這就是我想說的!
Functors的實現超出了本文的範圍,甚至不重要。不用說,我們非常接近從Java 8實現CompletableFuture,並且幾乎從RxJava中發現了Observable。但是回到Functors。Promise <客戶>尚未持有客戶的值。它有望在將來具有這種價值。但是,我們仍然可以像使用FOptional和FList一樣映射此類Functors-語法和語義完全相同。行為遵循Functors表示的內容。調用customer.map(Customer :: getAddress)會產生Promise <Address>,這意味著地圖是非阻塞的。customer.map()將客戶承諾完成。相反,它將返回另一個不同類型的promise。當上遊承諾完成後,下遊承諾應用傳遞給map()的函數並將結果傳遞給下遊。突然,我們的Functors使我們能夠以非阻塞方式流水線進行異步計算。但是您不必了解或學習-因為Promise是Functors,所以它必須遵循語法和法則。
Functors還有許多其他很好的例子,例如以組合方式表示值或錯誤。但是現在是時候看看Monads了。
從 Functors到Monads
我假設您了解Functors是如何工作的,為什麼它們是有用的抽象。但是Functors並不像人們期望的那樣普遍。如果您的轉換函數(作為map()的一個參數傳遞)返回Functors實例而不是簡單值,會發生什麼情況?好吧,Functors也是一個值,因此不會發生任何不良情況。將返回的所有內容放回Functors中,以便所有行為都保持一致。但是,假設您有以下方便的方法來解析字符串:
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()返回a,int您將得到FOptional<Integer> num,但是由於map()函數FOptional<Integer>本身返回,因此將其包裝兩次成尷尬FOptional<FOptional<Integer>>。請仔細查看類型,您必須了解為什麼我們在這裡得到這種雙重包裝。除了看上去很恐怖之外,在Functors中放一個Functors會破壞構圖和流暢的連結:
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));
在這裡,我們嘗試FOptional通過轉換int為+ Date + 映射內容。有了int -> Date我們可以輕鬆地從轉換Functor<Integer>為的功能Functor<Date>,我們知道它是如何工作的。但是在 num2 情況變得複雜的情況下。什麼num2.map()接收輸入的不再是一個int,但一個FOoption<Integer>顯然java.util.Date不具備這樣的構造。我們通過雙重包裹打破了Functors。但是,擁有返回Functors而不是簡單值的函數非常普遍(如tryParse()),我們不能簡單地忽略這種要求。一種方法是引入一種特殊的無參數join()方法,以「展平」嵌套Functors:
FOptional<Integer> num3 = num2.join()
它可以工作,但是因為這種模式太普遍了,所以flatMap()引入了名為的特殊方法。flatMap()與以下內容非常相似,map但希望作為參數接收的函數返回Functors-或準確地說是monad:
interface Monad<T,M extends Monad<?,?>> extends Functor<T,M> {
M flatMap(Function<T,M> f);
}
我們簡單地得出結論,這flatMap只是一種語法糖,可以使成分更好。但是flatMap方法(通常稱為Haskell bind或>>=從Haskell 調用)具有所有不同,因為它允許以純淨的功能樣式構成複雜的轉換。如果FOptional是monad的實例,則解析突然可以按預期進行:
FOptional<String> num = FOptional.of("42");
FOptional<Integer> answer = num.flatMap(this::tryParse);
Monads不需要實現map,它可以flatMap()很容易地實現。事實上flatMap,必不可少的運算符可實現全新的轉換領域。顯然,就像Functors一樣,句法順從性不足以將某類稱為Monads,flatMap()操作員必須遵守Monads法則,但是它們非常直觀,就像flatMap()與身份的結合一樣。後者要求m(x).flatMap(f)與f(x)持有值x和函數的任何monad 相同f。我們不會深入研究monad理論,而讓我們關注實際含義。例如,當單聲道內部結構不重要時,它們會發光Promise未來將具有價值的monad。您可以從類型系統中猜出Promise在以下程序中將如何運行嗎?首先,所有可能花費一些時間才能完成的方法都返回a Promise:
import java.time.DayOfWeek;
Promise<Customer> loadCustomer(int id) {
//...
}
Promise<Basket> readBasket(Customer customer) {
//...
}
Promise<BigDecimal> calculateDiscount(Basket basket, DayOfWeek dow) {
//...
}
現在,我們可以像使用monadic運算符一樣阻止所有這些函數的方式編寫這些函數:
Promise<BigDecimal> discount =
loadCustomer(42)
.flatMap(this::readBasket)
.flatMap(b -> calculateDiscount(b, DayOfWeek.FRIDAY));
這變得很有趣。flatMap()必須保留Monads類型,因此所有中間對象均為Promises。這不僅僅是保持類型有序-前一個程序突然完全異步!loadCustomer()返回一個,Promise因此它不會阻塞。readBasket()接受Promise具有(將具有)的任何東西,並應用返回另一個函數的函數Promise,依此類推。基本上,我們建立了一個異步計算管道,其中後臺完成一個步驟會自動觸發下一步。
探索 flatMap()
有兩個Monads並將它們包含的值組合在一起是很常見的。但是,Functors和monad都不允許直接訪問其內部,這是不純的。相反,我們必須謹慎地應用轉換,而不能逃脫monad。假設您有兩個Monads,並且想要將它們合併:
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強調核心概念。我們有兩個獨立的Monads,一個是type Month,另一個是type 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都一致地起作用。liftM2當您考慮如何將其與各種monad結合使用時,它非常有用。例如,listM2(list1, list2, function)將應用於和(笛卡爾積)function上的所有可能的項目對。另一方面,對於可選選項,僅當兩個可選選項均為非空時,它將應用功能。更好的是,對於 monad,當兩個都完成時,函數將異步執行。這意味著我們只是發明了一個簡單的同步機制(在fork-join算法中),該機制包含兩個異步步驟。list1list2Promise Promisejoin()
我們可以輕鬆構建的另一個有用的運算符flatMap()是filter(Predicate<T>),它接受monad中的所有內容,如果不符合某些謂詞,則將其完全丟棄。在某種程度上,它類似於map1-to-1映射,而不是1-to-1映射。同樣filter(),每個monad具有相同的語義,但取決於我們實際使用的monad,其功能卻非常出色。顯然,它允許從列表中過濾掉某些元素:
FList<Customer> vips =
customers.filter(c -> c.totalOrders > 1_000);
但是它也可以很好地工作,例如對於可選項目。在這種情況下,如果可選內容不符合某些條件,我們可以將非空可選轉換為空。空的可選部分保持不變。
從Monads列表到Monads列表
源自flatMap()的另一個有用的運算符是sequence()。您只需查看類型籤名即可輕鬆猜測其作用:
Monad<Iterable<T>> sequence(Iterable<Monad<T>> monads)
通常,我們有一堆相同類型的monad,而我們想要一個具有該類型列表的monad。這對您來說可能聽起來很抽象,但卻非常有用。想像一下,您想通過ID同時從資料庫中加載一些客戶,因此您loadCustomer(id)多次對不同的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,FList<Integer>我們擁有一個具有代表性的客戶ID map(您知道它對FList仿函數有何幫助?)database.loadCustomer(id)。這導致Promises的列表非常不便。sequence()節省了一天的時間,但這再次不僅僅是語法糖。前面的代碼是完全非阻塞的。對於不同種類的Monadssequence()還是有意義的,但是在不同的計算環境中。例如,它可以更改FList<FOptional<T>>為FOptional<FList<T>>。順便說一句,您可以在之上實現sequence()(就像map())flatMap()。
flatMap()一般而言,這只是冰山一角。儘管源於晦澀的類別理論,但即使在Java等面向對象的程式語言中,monad也被證明是極其有用的抽象。能夠組成返回Monads函數的函數非常有用,以至於數十個無關的類遵循Monads行為。
而且,一旦將數據封裝在monad中,通常很難顯式地將其取出。這種操作不是monad行為的一部分,並且經常導致非慣用語代碼。例如,Promise.get()on Promise<T>可以從技術上返回T,但只能通過阻塞返回,而所有基於的運算符flatMap()都是非阻塞的。另一個示例是FOptional.get(),但是可能失敗,因為FOptional可能為空。即使FList.get(idx)從列表中偷看特定元素也聽起來很尷尬,因為您可以經常替換for循環map()。
我希望您現在了解為什麼現在這些Monads如此流行。即使在像Java這樣的面向對象的語言中,它們也是非常有用的抽象。
最後,開發這麼多年我也總結了一套學習Java的資料與面試題,如果你在技術上面想提升自己的話,可以關注我,私信發送領取資料或者在評論區留下自己的聯繫方式,有時間記得幫我點下轉發讓跟多的人看到哦。