Java 中的單子
一、概述
在本教程中,我們將使用 Java 討論 Monad 及其定義。這個想法是為了理解這個概念,它解決了什麼問題,以及 Java 語言是如何實現它的。
到最後,我們希望人們能夠理解 Monad 以及如何充分利用它。
2.概念
Monad 是函數式編程世界中流行的一種設計模式。然而,它實際上起源於一個叫做範疇論的數學領域。本文將重點介紹 Monad 對軟件工程領域的定義。儘管這兩個定義有很多相似之處,但軟件定義和該領域的行話更符合我們的上下文。
簡而言之,一個通用的概念是一個對象,它可以根據變換將自己映射到不同的結果。
3. 設計模式
Monad 是封裝值和計算的容器或結構。它們必須有兩個基本操作:
-
Unit
: monads 代表一種包裝給定值的類型,這個操作負責包裝值。例如,在 Java 中,此操作僅通過利用泛型就可以接受來自不同類型的值 -
Bind
:此操作允許使用保存的值執行轉換並返回一個新的 monad 值(monad 類型中的值包裝)
儘管如此,還是有一些 monad 必須遵守的屬性:
- 左身份:當應用於 monad 時,它應該產生與將轉換應用於持有值相同的結果
- 正確的標識:發送 monad 轉換(將值轉換為 monad)時,yield 結果必須與將值包裝在新的 monad 中相同
- 關聯性:鏈接轉換時,轉換的嵌套方式無關緊要
函數式編程的挑戰之一是允許在不損失可讀性的情況下對此類操作進行流水線處理。這是採用 monad 概念的原因之一。 Monad 是函數式範例的基礎,有助於實現聲明式編程。
4.Java解釋
Java 8 通過Optional.
不過,我們先來看一段添加Optional
類之前的代碼:
public class MonadSample1 {
//...
private double multiplyBy2(double n) {
return n * 2;
}
private double divideBy2(double n) {
return n / 2;
}
private double add3(double n) {
return n + 3;
}
private double subtract1(double n) {
return n - 1;
}
public double apply(double n) {
return subtract1(add3(divideBy2(multiplyBy2(multiplyBy2(n)))));
}
//...
public class MonadSampleUnitTest {
//...
@Test
public void whenNotUsingMonad_shouldBeOk() {
MonadSample1 test = new MonadSample1();
Assert.assertEquals(6.0, test.apply(2), 0.000);
}
//...
}
我們可以觀察到, apply
方法看起來很難讀,但有什麼替代方法呢?也許是以下內容:
public class MonadSample2 {
//...
public double apply(double n) {
double n1 = multiplyBy2(n);
double n2 = multiplyBy2(n1);
double n3 = divideBy2(n2);
double n4 = add3(n3);
return subtract1(n4);
}
//...
public class MonadSampleUnitTest {
//...
@Test
public void whenNotUsingMonadButUsingTempVars_shouldBeOk() {
MonadSample2 test = new MonadSample2();
Assert.assertEquals(6.0, test.apply(2), 0.000);
}
//...
}
這似乎更好,但它看起來仍然過於冗長。那麼讓我們看看使用Optional
會是什麼樣子:
public class MonadSample3 {
//...
public double apply(double n) {
return Optional.of(n)
.flatMap(value -> Optional.of(multiplyBy2(value)))
.flatMap(value -> Optional.of(multiplyBy2(value)))
.flatMap(value -> Optional.of(divideBy2(value)))
.flatMap(value -> Optional.of(add3(value)))
.flatMap(value -> Optional.of(subtract1(value)))
.get();
}
//...
public class MonadSampleUnitTest {
//...
@Test
public void whenUsingMonad_shouldBeOk() {
MonadSample3 test = new MonadSample3();
Assert.assertEquals(6.0, test.apply(2), 0.000);
}
//...
}
上面的代碼看起來更乾淨。另一方面,這種設計允許開發人員根據需要應用盡可能多的後續轉換,而不會犧牲可讀性和減少臨時變量聲明的冗長。
還有更多;想像一下,如果這些函數中的任何一個可以產生null
值。在這種情況下,我們將不得不在每次轉換之前添加驗證,從而使代碼更加冗長。這實際上是Optional
類的主要目的。這個想法是為了避免使用null
並提供一種易於使用的方法來將轉換應用於對象,以便當它們不為null,
將以空安全的方式執行一系列聲明。也可以檢查Optional
包裝的值是否為空(值為 null)。
4.1.可選注意事項
正如開頭所描述的,Monads需要有一些操作和屬性,那麼讓我們看看Java實現中的那些屬性。首先,為什麼不檢查 Monad 必須具有的操作:
- 對於
Unit
操作,Java 提供了不同的風格,例如Optional.of()
和**Optional.nullable()** .
正如我們想像的那樣,一個接受null
值,另一個不接受 - 至於
Bind
函數,Java 提供了Optional.flatMap()
操作,在代碼示例中介紹
一個不在 Monad 定義中的特性是map
操作。它是一種類似於flatMap
的轉換和鍊式操作。兩者之間的區別在於map
操作接收一個轉換,該轉換返回一個原始值,由 API 在內部包裝。雖然flatMap
已經返回一個包裝值,API 返回該值以形成管道。
現在,讓我們檢查一下 Monad 的屬性:
public class MonadSample4 {
//...
public boolean leftIdentity() {
Function<Integer, Optional> mapping = value -> Optional.of(value + 1);
return Optional.of(3).flatMap(mapping).equals(mapping.apply(3));
}
public boolean rightIdentity() {
return Optional.of(3).flatMap(Optional::of).equals(Optional.of(3));
}
public boolean associativity() {
Function<Integer, Optional> mapping = value -> Optional.of(value + 1);
Optional leftSide = Optional.of(3).flatMap(mapping).flatMap(Optional::of);
Optional rightSide = Optional.of(3).flatMap(v -> mapping.apply(v).flatMap(Optional::of));
return leftSide.equals(rightSide);
}
//...
public class MonadSampleUnitTest {
//...
@Test
public void whenTestingMonadProperties_shouldBeOk() {
MonadSample4 test = new MonadSample4();
Assert.assertEquals(true, test.leftIdentity());
Assert.assertEquals(true, test.rightIdentity());
Assert.assertEquals(true, test.associativity());
}
//...
}
乍一看,所有的屬性似乎都符合要求,Java 也有妥善的 Monad 實現,但實際上並非如此。讓我們再做一個測試:
class MonadSample5 {
//...
public boolean fail() {
Function<Integer, Optional> mapping = value -> Optional.of(value == null ? -1 : value + 1);
return Optional.ofNullable((Integer) null).flatMap(mapping).equals(mapping.apply(null));
}
//...
class MonadSampleUnitTest {
@Test
public void whenBreakingMonadProperties_shouldBeFalse() {
MonadSample5 test = new MonadSample5();
Assert.assertEquals(false, test.fail());
}
//...
}
正如所觀察到的,Monad 的左標識屬性被破壞了。實際上,根據這個討論,這似乎是一個有意識的決定。 JDK 團隊的一位成員說Optional
的範圍比其他語言更窄,他們不打算超過這個範圍。在其他情況下,此類屬性可能不成立。
實際上,其他 API,如流,也有類似的設計,但並不打算完全實現 Monad 規範。
5.結論
在本文中,我們了解了 Monad 的概念、它們是如何在 Java 中引入的,以及這種實現的細微差別。
可以爭辯說 Java 實際上並不是 Monad 實現,並且在設計空安全時,他們違反了原則。但是,這種模式的許多好處仍然存在。
與往常一樣,本文中使用的所有代碼示例都可以在 GitHub 上找到。