在本系列的前四篇文章中對函數式編程進行了多方位的介紹。本文將着重介紹函數式編程中一個重要而又複雜的概念:Monad。一直以來,Monad 都是函數式編程中最具備神祕色彩的概念。正如 JSON 格式的提出者 Douglas Crockford 所指出的,Monad 有一種魔咒,一旦你真正理解了它的意義,就失去了解釋給其餘人的能力。本文嘗試深刻解析 Monad 這一律念。因爲 Monad 的概念會涉及到一些數學理論,可能讀起來會比較枯燥。本文側重在 Monad 與編程相關的方面,並結合 Java 示例代碼來進行說明。html
要解釋 Monad,就必須提到範疇論(Category Theory)。範疇(category)自己是一個很簡單的概念。一個範疇由對象(object)以及對象之間的箭頭(arrow)組成。範疇的核心是組合,體如今箭頭的組合性上。若是從對象 A 到對象 B 有一個箭頭,從對象 B 到對象 C 也有一個箭頭,那麼必然有一個從對象 A 到對象 C 的箭頭。從 A 到 C 的這個箭頭,就是 A 到 B 的箭頭和 B 到 C 的箭頭的組合。這種組合的必然存在性,是範疇的核心特徵。以專業術語來講,箭頭被稱爲態射(morphisms)。範疇中對象和箭頭的概念能夠很容易地映射到函數中。類型能夠做爲範疇中的對象,把函數當作是箭頭。若是有一個函數 f 的參數類型是 A,返回值類型是 B,那麼這個函數是從 A 到 B 的態射;另一個函數 g 的參數類型是 B,返回值類型是 C,這個函數是從 B 到 C 的態射。能夠把 f 和 g 組合起來,獲得一個新的從類型 A 到類型 C 的函數,記爲 g ∘f,也就是從 A 到 C 的態射。這種函數的組合方式是必然存在的。java
一個範疇中的組合須要知足兩個條件:git
從編程的角度來講,範疇論的概念要求在設計時應該考慮對象的接口,而不是具體的實現。範疇論中的對象很是的抽象,沒有關於對象的任何定義。咱們只知道對象上的箭頭,而對於對象自己則一無所知。對象其實是由它們之間的相互組合關係來定義的。github
範疇的概念雖然抽象,實際上也很容易找到現實的例子。最直接的例子是從有向圖中建立出範疇。對於有向圖中的每一個節點,首先添加一個從當前節點到自身的箭頭。而後對於每兩條首尾相接的邊,添加一條新的箭頭鏈接起始和結束節點。如此反覆,就獲得了一個範疇。編程
範疇中的對象和態射的概念很抽象。從編程的角度來講,咱們能夠找到更好的表達方式。在程序中,討論單個的對象實例並無意義,更重要的是對象的類型。在各類編程語言中,咱們已經認識了不少類型,包括 int、long、double 和 char 等。類型能夠當作是值的集合。好比 bool 類型就只有兩個值 true 和 false,int 類型包含全部的整數。類型的值能夠是有限的,也能夠是無限的。好比 String 類型的值是無限的。編程語言中的函數實際上是從類型到類型的映射。對於參數超過 1 個的函數,老是可使用柯里化來轉換爲只有一個參數的函數。數組
類型和函數能夠分別與範疇中的對象和態射相對應。範疇中的對象是類型,而態射則是函數。類型的做用在於限定了範疇中態射能夠組合的方式,也就是函數的組合方式。只有一個函數的返回值類型與另外一個函數的參數類型匹配時,這兩個函數才能並確定能夠組合。這也就知足了範疇的定義。app
以前討論的函數都是純函數,不含任何反作用。而在實際的編程中,是離不開反作用的。純函數適合於描述計算,可是沒辦法描述輸出字符串到控制檯或是寫數據到文件這樣的反作用。Monad 的做用正是解決了如何描述反作用的問題。實際上,純粹的函數式編程語言 Haskell 正是用 Monad 來處理描述 IO 等基於反作用的操做。在介紹 Monad 以前,須要先說明 Functor。編程語言
Functor 是範疇之間的映射。對於兩個範疇 A 和 B,Functor F 把範疇 A 中的對象映射到範疇 B 中。Functor 在映射時會保留對象之間的鏈接關係。若是範疇 A 中存在從對象 a 到對象 b 的態射,那麼 a 和 b 通過 Functor F 在範疇 B 中的映射值 F a 和 F b 之間也存在着態射。一樣的,態射之間的組合關係,以及恆等態射都會被保留。因此說 Functor 不只是範疇中對象之間的映射,也是態射之間的映射。若是一個 Functor 從一個範疇映射到本身,稱爲 endofunctor。ide
前面提到過,編程語言中的範疇中的對象是類型,而態射是函數。所以,這樣的 endofunctor 是從類型到類型的映射,同時也是函數到函數的映射。咱們首先看一個具體的 Functor :Option。Option 的定義很簡單,Java 標準庫和 Vavr 中都有對應的類。不過咱們這裏討論的 Option 與 Java 中的 Optional 類有很大不一樣。Option 自己是一個類型構造器,使用時須要提供一個類型,所獲得的結果是另一個新的類型。這裏能夠與 Java 中的泛型做爲類比。Option 有兩種可能的值:Some 和 None。Some 表示對應類型的一個值,而 None 表示沒有值。對於一個從 a 到 b 的映射 f,能夠很容易地找到與之對應的使用 Option 的映射。該映射把 None 對應到 None,而把 f(Some a)映射到 Some f(a)。函數式編程
Monad 自己也是一種 Functor。Monad 的目的在於描述反作用。
清單 1 給出了一個簡單的函數 increase。該函數的做用是返回輸入的參數加 1 以後的值。除了進行計算以外,還經過 count++來修改一個變量的值。這行語句的出現,使得函數 increase 再也不是純函數,每次調用都會對外部環境形成影響。
int count = 0; int increase(int x) { count++; return x + 1;}
清單 1 中的函數 increase 能夠劃分紅兩個部分:產生反作用的 count++,以及剩餘的不產生反作用的部分。若是能夠經過一些轉換,把反作用從函數 increase 中剝離出來,那麼就能夠獲得另一個純函數的版本 increase1,如清單 2 所示。對函數 increase1 來講,咱們能夠把返回值改爲一個 Vavr 中的 Tuple2<Integer, Integer> 類型,分別包含函數原始的返回值 x + 1 和在 counter 上增長的增量值 1。經過這樣的轉換以後,函數 increase1 就變成了一個純函數。
Tuple2<Integer, Integer> increase1(int x) { return Tuple.of(x + 1, 1);}
在通過這樣的轉換以後,對於函數 increase1 的調用方式也發生了變化,如清單 3 所示。遞增以後的值須要從 Tuple2 中獲取,而 count 也須要經過 Tuple2 的值來更新。
int x = 0;Tuple2<Integer, Integer> result = increase1(x);x = result._1;count += result._2;
咱們能夠採用一樣的方式對另一個類似的函數 decrease 作轉換,如清單 4 所示。
int decrease(int x) { count++; return x - 1;} Tuple2<Integer, Integer> decrease1(int x) { return Tuple.of(x - 1, 1);}
不過須要注意的是,通過這樣的轉換以後,函數的組合方式發生了變化。對於以前的 increase 和 decrease 函數,能夠直接組合,由於它們的參數和返回值類型是匹配的,如相似 increase(decrease(x)) 或是 decrease(increase(x)) 這樣的組合方式。而通過轉換以後的 increase1 和 decrease1,因爲返回值類型改變,increase1 和 decrease1 不能按照以前的方式進行組合。函數 increase1 的返回值類型與 decrease1 的參數類型不匹配。對於這兩個函數,須要另外的方式來組合。
在清單 5 中,compose 方法把兩個類型爲 Function<Integer, Tuple2<Integer, Integer>> 的函數 func1 和 func2 進行組合,返回結果是另一個類型爲 Function<Integer, Tuple2<Integer, Integer>> 的函數。在進行組合時,Tuple2 的第一個元素是實際須要返回的結果,按照純函數組合的方式來進行,也就是把 func1 調用結果的 Tuple2 的第一個元素做爲輸入參數來調用 func2。Tuple2 的第二個元素是對 count 的增量。須要把這兩個增量相加,做爲 compose 方法返回的 Tuple2 的第二個元素。
Function<Integer, Tuple2<Integer, Integer>> compose( Function<Integer, Tuple2<Integer, Integer>> func1, Function<Integer, Tuple2<Integer, Integer>> func2) { return x -> { Tuple2<Integer, Integer> result1 = func1.apply(x); Tuple2<Integer, Integer> result2 = func2.apply(result1._1); return Tuple.of(result2._1, result1._2 + result2._2); };}
清單 6 中的 doCompose 函數對 increase1 和 decrease1 進行組合。對於一個輸入 x,因爲 increase1 和 decrease1 的做用相互抵消,獲得的結果是值爲 (x, 2) 的對象。
Tuple2<Integer, Integer> doCompose(int x) { return compose(this::increase1, this::decrease1).apply(x);}
能夠看到,doCompose 函數的輸入參數和返回值類型與 increase1 和 decrease1 相同。所返回的結果能夠繼續使用 doCompose 函數來與其餘類型相同的函數進行組合。
如今回到函數 increase 和 decrease。從範疇論的角度出發,咱們考慮下面一個範疇。該範疇中的對象仍然是 int 和 bool 等類型,可是其中的態射再也不是簡單的如 increase 和 decrease 這樣的函數,而是把這些函數經過相似從 increase 到 increase1 這樣的方式轉換以後的函數。範疇中的態射必須是能夠組合的,而這些函數的組合是經過調用相似 doCompose 這樣的函數完成的。這樣就知足了範疇的第一條原則。而第二條原則也很容易知足,只須要把參數 x 的值設爲 0,就能夠獲得組合的基本單元。由此能夠得出,咱們定義了一個新的範疇,而這個範疇就叫作 Kleisli 範疇。每一個 Kleisli 範疇所使用的函數轉換方式是獨特的。清單 2中的示例使用 Tuple2 來保存 count 的增量。與之對應的,Kleisli 範疇中對態射的組合方式也是獨特的,相似清單 6 中的 doCompose 函數。
在對 Kleisli 範疇有了一個直觀的瞭解以後,就能夠對 Monad 給出一個形式化的定義。給定一個範疇 C 和 endofunctor m,與之相對應的 Kleisli 範疇中的對象與範疇 C 相同,但態射是不一樣的。K 中的兩個對象 a 和 b 之間的態射,是由範疇 C 中的 a 到 m(b) 的態射來實現的。注意,Kleisli 範疇 K 中的態射箭頭是從對象 a 到對象 b 的,而不是從對象 a 到 m(b)。若是存在一種傳遞的組合方式,而且每一個對象都有組合單元箭頭,也就是知足範疇的兩大原則,那麼這個 endofunctor m 就叫作 Monad。
一個 Monad 的定義中包含了 3 個要素。在定義 Monad 時須要提供一個類型構造器 M 和兩個操做 unit 和 bind:
若是隻看 Monad 的定義,會有點晦澀難懂。實際上清單 2 中的示例就是一種常見的 Monad,稱爲 Writer Monad。下面咱們結合 Java 代碼來看幾種常見的 Monad。
清單 2 展現了 Writer Monad 的一種用法,也就是累積 count 的值。實際上,Writer Monad 的主要做用是在函數調用過程當中收集輔助信息,好比日誌信息或是性能計數器等。其基本的思想是把反作用中對外部環境的修改聚合起來,從而把反作用從函數中分離出來。聚合的方式取決於所產生的反作用。清單 2中的反作用是修改計算器 count,相應的聚合方式是累加計數值。若是反作用是產生日誌,相應的聚合方式是鏈接日誌記錄的字符串。聚合方式是每一個 Writer Monad 的核心。對於聚合方式的要求和範疇中對於態射的要求是同樣,也就是必須是傳遞的,並且有組合的基本單元。在清單 5中,聚合方式是 Integer 類型的相加操做,是傳遞的;同時也有基本單元,也就是加零。
下面對 Writer Monad 進行更加形式化的說明。Writer Monad 除了其自己的類型 T 以外,還有另一個輔助類型 W,用來表示聚合值。對類型 W 的要求是前面提到的兩點,也就是存在傳遞的組合操做和基本單元。Writer Monad 的 unit 操做比較簡單,返回的是類型 T 的值 t 和類型 W 的基本單元。而 bind 操做則須要分別轉換類型 T 和 W 的值。對於 T 的值,按照 Monad 自身的定義來轉換;而對於 W 的值,則使用該類型的傳遞操做來聚合值。聚合的結果做爲轉換以後的新的 W 的值。
清單 7 中是記錄日誌的 Writer Monad 的實例。該 Monad 自身的類型使用 Java 泛型類型 T 來表示,而輔助類型是 List<String>,用來保存記錄的日誌。List<String> 知足做爲輔助類型的要求。List<String> 上的相加操做是傳遞的,也存在做爲基本單元的空列表。LoggingMonad 中的 unit 方法返回傳入的值 value 和空列表。bind 方法的第一個參數是 LoggingMonad<T1> 類型,做爲變換的輸入;第二個參數是 Function<T1, LoggingMonad<T2>> 類型,用來把類型 T1 轉換成新的 LoggingMonad<T2> 類型。輔助類型 List<String> 中的值經過列表相加的方式進行組合。方法 pipeline 表示一個處理流水線,對於一個輸入 Monad,依次應用指定的變換,獲得最終的結果。在使用示例中,LoggingMonad 中封裝的是 Integer 類型,第一個轉換把值乘以 4,第二個變換把值除以 2。每一個變換都記錄本身的日誌。在運行流水線以後,獲得的結果包含了轉換以後的值和聚合的日誌。
public class LoggingMonad<T> { private final T value; private final List<String> logs; public LoggingMonad(T value, List<String> logs) { this.value = value; this.logs = logs; } @Override public String toString() { return "LoggingMonad{" + "value=" + value + ", logs=" + logs + '}'; } public static <T> LoggingMonad<T> unit(T value) { return new LoggingMonad<>(value, List.of()); } public static <T1, T2> LoggingMonad<T2> bind(LoggingMonad<T1> input, Function<T1, LoggingMonad<T2>> transform) { final LoggingMonad<T2> result = transform.apply(input.value); List<String> logs = new ArrayList<>(input.logs); logs.addAll(result.logs); return new LoggingMonad<>(result.value, logs); } public static <T> LoggingMonad<T> pipeline(LoggingMonad<T> monad, List<Function<T, LoggingMonad<T>>> transforms) { LoggingMonad<T> result = monad; for (Function<T, LoggingMonad<T>> transform : transforms) { result = bind(result, transform); } return result; } public static void main(String[] args) { Function<Integer, LoggingMonad<Integer>> transform1 = v -> new LoggingMonad<>(v * 4, List.of(v + " * 4")); Function<Integer, LoggingMonad<Integer>> transform2 = v -> new LoggingMonad<>(v / 2, List.of(v + " / 2")); final LoggingMonad<Integer> result = pipeline(LoggingMonad.unit(8), List.of(transform1, transform2)); System.out.println(result); // 輸出爲 LoggingMonad{value=16, logs=[8 * 4, 32 / 2]} }}
Reader Monad 也被稱爲 Environment Monad,描述的是依賴共享環境的計算。Reader Monad 的類型構造器從類型 T 中建立出一元類型 E → T,而 E 是環境的類型。類型構造器把類型 T 轉換成一個從類型 E 到 T 的函數。Reader Monad 的 unit 操做把類型 T 的值 t 轉換成一個永遠返回 t 的函數,而忽略類型爲 E 的參數;bind 操做在轉換時,在所返回的函數的函數體中對類型 T 的值 t 進行轉換,同時保持函數的結構不變。
清單 8 是 Reader Monad 的示例。Function<E, T> 是一元類型的聲明。ReaderMonad 的 unit 方法返回的 Function 只是簡單的返回參數值 value。而 bind 方法的第一個參數是一元類型 Function<E, T1>,第二個參數是把類型 T1 轉換成 Function<E, T2> 的函數,返回值是另一個一元類型 Function<E, T2>。bind 方法的轉換邏輯首先經過 input.apply(e) 來獲得類型爲 T1 的值,再使用 transform.apply 來獲得類型爲 Function<E, T2>> 的值,最後使用 apply(e) 來獲得類型爲 T2 的值。
public class ReaderMonad { public static <T, E> Function<E, T> unit(T value) { return e -> value; } public static <T1, T2, E> Function<E, T2> bind(Function<E, T1>input, Function<T1, Function<E, T2>> transform) { return e -> transform.apply(input.apply(e)).apply(e); } public static void main(String[] args) { Function<Environment, String> m1 = unit("Hello"); Function<Environment, String> m2 = bind(m1, value -> e -> e.getPrefix() + value); Function<Environment, Integer> m3 = bind(m2, value -> e -> e.getBase() + value.length()); int result = m3.apply(new Environment()); System.out.println(result); }}
清單 8 中使用的環境類型 Environment 如清單 9 所示,其中有兩個方法 getPrefix 和 getBase 分別返回相應的值。清單 8 的 m1 是值爲 Hello 的單元類型,m2 使用了 Environment 的 getPrefix 方法進行轉換,而 m3 使用了 getBase 方法進行轉換,最終輸出的結果是 107。由於字符串 Hello 在添加了前綴 $$ 以後的長度是 7,與 100 相加以後的值是 107。
public class Environment { public String getPrefix() { return "$$"; } public int getBase() { return 100; }}
State Monad 能夠在計算中附加任意類型的狀態值。State Monad 與 Reader Monad 類似,只是 State Monad 在轉換時會返回一個新的狀態對象,從而能夠描述可變的環境。State Monad 的類型構造器從類型 T 中建立一個函數類型,該函數類型的參數是狀態對象的類型 S,而返回值包含類型 S 和 T 的值。State Monad 的 unit 操做返回的函數只是簡單地返回輸入的類型 S 的值;bind 操做所返回的函數類型負責在執行時傳遞正確的狀態對象。
清單 10 給出了 State Monad 的示例。State Monad 使用元組 Tuple2<T, S> 來保存計算值和狀態對象,所對應的一元類型是 Function<S, Tuple2<T, S>> 表示的函數。unit 方法所返回的函數只是簡單地返回輸入狀態對象。bind 方法的轉換邏輯使用 input.apply(s) 獲得 T1 和 S 的值,再用獲得的 S 值調用 transform。
public class StateMonad { public static <T, S> Function<S, Tuple2<T, S>> unit(T value) { return s -> Tuple.of(value, s); } public static <T1, T2, S> Function<S, Tuple2<T2, S>> bind(Function<S, Tuple2<T1, S>> input, Function<T1, Function<S, Tuple2<T2, S>>> transform) { return s -> { Tuple2<T1, S> result = input.apply(s); return transform.apply(result._1).apply(result._2); }; } public static void main(String[] args) { Function<String, Function<String, Function<State, Tuple2<String, State>>>> transform = prefix -> value -> s -> Tuple .of(prefix + value, new State(s.getValue() + value.length())); Function<State, Tuple2<String, State>> m1 = unit("Hello"); Function<State, Tuple2<String, State>> m2 = bind(m1, transform.apply("1")); Function<State, Tuple2<String, State>> m3 = bind(m2, transform.apply("2")); Tuple2<String, State> result = m3.apply(new State(0)); System.out.println(result); }}
State Monad 中使用的狀態對象如清單 11 所示。State 是一個包含值 value 的不可變對象。清單 10 中的 m1 封裝了值 Hello。transform 方法用來從輸入的字符串前綴 prefix 中建立轉換函數。轉換函數會在字符串值上添加給定的前綴,同時會把字符串的長度進行累加。轉換函數每次都返回一個新的 State 對象。轉換以後的結果中字符串的值是 21Hello,而 State 對象中的 value 爲 11,是字符串 Hello 和 1Hello 的長度相加的結果。
public class State { private final int value; public State(final int value) { this.value = value; } public int getValue() { return value; } @Override public String toString() { return "State{" + "value=" + value + '}'; }}
做爲本系列的最後一篇文章,本文對函數式編程中的重要概念 Monad 作了詳細的介紹。本文從範疇論出發,介紹了使用 Monad 描述函數反作用的動機和方式,以及 Monad 的定義。本文還對常見的幾種 Monad 進行了介紹,並添加了相應的 Java 代碼。
原做者:成 富
原文連接: 深刻解析 Monad
原出處:IBM Developer