JavaScript、Python、Ruby、Swift 和 Scala 中的 Option/Maybe、Either 和 Future Monad
已發表: 2022-03-11本 monad 教程簡要解釋了 monad,並展示瞭如何在五種不同的編程語言中實現最有用的 monad——如果你正在尋找 JavaScript 中的 monad、Python 中的 monad、Ruby 中的 monad、Swift 中的 monad 和/或 monad在 Scala 中,或比較任何實現,您正在閱讀正確的文章!
使用這些 monad,您將擺脫一系列錯誤,例如空指針異常、未處理的異常和競爭條件。
這就是我在下面介紹的內容:
- 範疇論導論
- 單子的定義
- Option (“Maybe”) monad、Either monad 和 Future monad 的實現,以及利用它們的示例程序,在 JavaScript、Python、Ruby、Swift 和 Scala 中
讓我們開始吧! 我們的第一站是范疇論,它是單子的基礎。
範疇論導論
範疇論是20世紀中葉積極發展的數學領域。 現在它是許多函數式編程概念的基礎,包括 monad。 讓我們快速瀏覽一些針對軟件開發術語調整的類別理論概念。
所以定義一個類別有三個核心概念:
- 類型就像我們在靜態類型語言中看到的那樣。 示例:
Int
、String
、Dog
、Cat
等。 - 函數連接兩種類型。 因此,它們可以表示為從一種類型到另一種類型或指向它們自己的箭頭。 從類型$T$ 到類型$U$ 的函數$f$ 可以表示為$f: T \to U$。 您可以將其視為一種編程語言函數,它接受 $T$ 類型的參數並返回 $U$ 類型的值。
- 組合是一種操作,由 $\cdot$ 運算符表示,它從現有功能構建新功能。 在一個範疇中,總是保證任何函數 $f: T \to U$ 和 $g: U \to V$ 都存在一個唯一的函數 $h: T \to V$。 這個函數記為$f \cdot g$。 該操作有效地將一對函數映射到另一個函數。 當然,在編程語言中,這種操作總是可能的。 例如,如果你有一個返回字符串長度的函數——$strlen: String \to Int$——和一個判斷數字是否為偶數的函數——$even: Int \to Boolean$——那麼你可以創建一個function $even{\_}strlen: String \to Boolean$ 判斷
String
的長度是否為偶數。 在這種情況下,$even{\_}strlen = even \cdot strlen$。 組合意味著兩個特徵:- 結合性:$f \cdot g \cdot h = (f \cdot g) \cdot h = f \cdot (g \cdot h)$
- 恆等函數的存在: $\forall T: \exists f: T \to T$,或者用簡單的英語來說,對於每個類型 $T$ 都存在一個將 $T$ 映射到自身的函數。
那麼讓我們來看一個簡單的類別。
旁注:我們假設這裡的Int
、 String
和所有其他類型都保證為非空,即不存在空值。
旁注 2:這實際上只是一個類別的一部分,但這就是我們討論的全部內容,因為它包含我們需要的所有基本部分,並且圖表不那麼混亂。 真正的類別也將具有所有組合函數,如 $roundToString: Double \to String = intToString \cdot round$,以滿足類別的組合子句。
您可能會注意到此類別中的函數非常簡單。 事實上,在這些功能中幾乎不可能出現錯誤。 沒有空值,沒有例外,只有算術和內存處理。 所以唯一可能發生的壞事是處理器或內存故障——在這種情況下,無論如何你都需要讓程序崩潰——但這種情況很少發生。
如果我們所有的代碼都在這種穩定性級別上工作,那不是很好嗎? 絕對地! 但是例如 I/O 呢? 我們絕對不能沒有它。 這就是 monad 解決方案的用武之地:它們將所有不穩定的操作隔離為超小且經過良好審核的代碼片段——然後您可以在整個應用程序中使用穩定的計算!
輸入單子
讓我們將 I/O 之類的不穩定行為稱為副作用。 現在我們希望能夠在存在這種副作用的情況下以穩定的方式使用我們之前定義的所有函數,如length
和String
等類型。
因此,讓我們從一個空類別 $M[A]$ 開始,並將其變成一個類別,該類別將具有具有一種特定類型的副作用的值以及沒有副作用的值。 假設我們已經定義了這個類別並且它是空的。 現在我們不能用它做任何有用的事情,所以為了讓它有用,我們將遵循以下三個步驟:
- 用 $A$ 類別中的類型值填充它,例如
String
、Int
、Double
等(下圖中的綠色框) - 一旦我們有了這些值,我們仍然無法對它們做任何有意義的事情,所以我們需要一種方法來從 $A$ 中獲取每個函數 $f: T \to U$ 並創建一個函數 $g: M[T] \to M [U]$(下圖中的藍色箭頭)。 一旦我們有了這些函數,我們就可以使用類別 $M[A]$ 中的值來完成我們能夠在類別 $A$ 中執行的所有操作。
- 現在我們有了一個全新的 $M[A]$ 類別,出現了一個帶有簽名 $h 的新函數類別:T \to M[U]$(下圖中的紅色箭頭)。 它們是作為我們代碼庫的一部分在第一步中提升價值的結果,即我們根據需要編寫它們; 這些是區分使用 $M[A]$ 和使用 $A$ 的主要因素。 最後一步是使這些函數在 $M[A]$ 中的類型上也能正常工作,即能夠從 $h: T \ 派生函數 $m: M[T] \to M[U]$到 M[U]$
因此,讓我們首先定義兩種將 $A$ 類型的值提升為 $M[A]$ 類型的值的方法:一種沒有副作用的函數,另一種有副作用的函數。
- 第一個稱為$pure$,它為穩定類別的每個值定義:$pure: T \to M[T]$。 生成的 $M[T]$ 值不會有任何副作用,因此這個函數稱為 $pure$。 例如,對於 I/O monad,$pure$ 將立即返回一些值,而不會失敗。
- 第二個稱為 $constructor$,與 $pure$ 不同,它返回 $M[T]$ 並帶有一些副作用。 一個用於異步 I/O monad 的 $constructor$ 的示例可以是從 Web 獲取一些數據並將其作為
String
返回的函數。 在這種情況下,$constructor$ 返回的值將具有 $M[String]$ 類型。
既然我們有兩種將值提升為 $M[A]$ 的方法,那麼作為程序員,您可以根據您的程序目標來選擇要使用的函數。 讓我們在這裡考慮一個示例:您想要獲取一個 HTML 頁面,例如 https://www.toptal.com/javascript/option-maybe-either-future-monads-js,為此您創建了一個函數 $fetch$。 因為在獲取它時任何事情都可能出錯——想想網絡故障等——你將使用 $M[String]$ 作為這個函數的返回類型。 所以它看起來像 $fetch: String \to M[String]$ 並且在函數體的某個地方我們將使用 $constructor$ 來代替 $M$。
現在讓我們假設我們製作了一個用於測試的模擬函數:$fetchMock: String \to M[String]$。 它仍然具有相同的簽名,但這次我們只是將生成的 HTML 頁面注入到 $fetchMock$ 的主體中,而不進行任何不穩定的網絡操作。 所以在這種情況下,我們只需在 $fetchMock$ 的實現中使用 $pure$。
下一步,我們需要一個函數來安全地將任意函數 $f$ 從類別 $A$ 提升到 $M[A]$(圖中的藍色箭頭)。 這個函數叫做$map: (T \to U) \to (M[T] \to M[U])$。
現在我們有了一個類別(如果我們使用 $constructor$,它可能會產生副作用),它還具有穩定類別的所有功能,這意味著它們在 $M[A]$ 中也是穩定的。 你可能注意到我們明確地引入了另一類函數,例如 $f: T \to M[U]$。 例如,$pure$ 和 $constructor$ 是 $U = T$ 的此類函數的示例,但顯然可能還有更多,例如我們先使用 $pure$ 然後使用 $map$。 所以,一般來說,我們需要一種方法來處理形式為 $f: T \to M[U]$ 的任意函數。
如果我們想基於 $f$ 創建一個可以應用於 $M[T]$ 的新函數,我們可以嘗試使用 $map$。 但這將把我們帶到函數 $g: M[T] \to M[M[U]]$,這是不好的,因為我們不想再有一個類別 $M[M[A]]$。 為了解決這個問題,我們引入最後一個函數:$flatMap: (T \to M[U]) \to (M[T] \to M[U])$。
但是我們為什麼要這樣做呢? 假設我們在第 2 步之後,即我們有 $pure$、$constructor$ 和 $map$。 假設我們想從 toptal.com 抓取一個 HTML 頁面,然後掃描那裡的所有 URL 並獲取它們。 我會創建一個函數 $fetch: String \to M[String]$ ,它只獲取一個 URL 並返回一個 HTML 頁面。
然後我將此函數應用到一個 URL 並從 toptal.com 獲取一個頁面,即 $x: M[String]$。 現在,我對 $x$ 進行了一些轉換,最後得到了某個 URL $u: M[String]$。 我想對它應用函數 $fetch$,但我不能,因為它需要類型 $String$,而不是 $M[String]$。 這就是為什麼我們需要 $flatMap$ 來將 $fetch: String \to M[String]$ 轉換為 $m_fetch: M[String] \to M[String]$。
現在我們已經完成了所有三個步驟,我們實際上可以組合我們需要的任何值轉換。 例如,如果您有 $M[T]$ 類型的值 $x$ 和 $f: T \to U$,則可以使用 $map$ 將 $f$ 應用於值 $x$ 並獲得值 $y$ $M[U]$ 類型。 這樣,只要 $pure$、$constructor$、$map$ 和 $flatMap$ 實現沒有錯誤,任何值的轉換都可以 100% 無錯誤地完成。
因此,不必每次在代碼庫中遇到一些討厭的效果時都處理它們,您只需要確保只有這四個函數被正確實現。 在程序結束時,您將只得到一個 $M[X]$,您可以在其中安全地解開值 $X$ 並處理所有錯誤情況。
這就是 monad:實現 $pure$、$map$ 和 $flatMap$ 的東西。 (其實 $map$ 可以從 $pure$ 和 $flatMap$ 派生出來,但它是非常有用和廣泛使用的函數,所以我沒有在定義中省略它。)
Option Monad,又名 Maybe Monad
好的,讓我們深入了解 monad 的實際實現和使用。 第一個真正有用的 monad 是 Option monad。 如果您來自經典編程語言,您可能會因為臭名昭著的空指針錯誤而遇到許多崩潰。 null 的發明者 Tony Hoare 稱這項發明為“十億美元的錯誤”:
這導致了無數的錯誤、漏洞和系統崩潰,在過去的四十年中可能造成了十億美元的痛苦和損失。
因此,讓我們嘗試對此進行改進。 Option monad 要么持有一些非空值,要么沒有值。 非常類似於空值,但是有了這個 monad,我們可以安全地使用我們定義良好的函數,而不必擔心空指針異常。 讓我們看一下不同語言的實現:
JavaScript——選項 Monad/Maybe Monad
class Monad { // pure :: a -> M a pure = () => { throw "pure method needs to be implemented" } // flatMap :: # M a -> (a -> M b) -> M b flatMap = (x) => { throw "flatMap method needs to be implemented" } // map :: # M a -> (a -> b) -> M b map = f => this.flatMap(x => new this.pure(f(x))) } export class Option extends Monad { // pure :: a -> Option a pure = (value) => { if ((value === null) || (value === undefined)) { return none; } return new Some(value) } // flatMap :: # Option a -> (a -> Option b) -> Option b flatMap = f => this.constructor.name === 'None' ? none : f(this.value) // equals :: # M a -> M a -> boolean equals = (x) => this.toString() === x.toString() } class None extends Option { toString() { return 'None'; } } // Cached None class value export const none = new None() Option.pure = none.pure export class Some extends Option { constructor(value) { super(); this.value = value; } toString() { return `Some(${this.value})` } }
Python——選項 Monad/Maybe Monad
class Monad: # pure :: a -> M a @staticmethod def pure(x): raise Exception("pure method needs to be implemented") # flat_map :: # M a -> (a -> M b) -> M b def flat_map(self, f): raise Exception("flat_map method needs to be implemented") # map :: # M a -> (a -> b) -> M b def map(self, f): return self.flat_map(lambda x: self.pure(f(x))) class Option(Monad): # pure :: a -> Option a @staticmethod def pure(x): return Some(x) # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(self, f): if self.defined: return f(self.value) else: return nil class Some(Option): def __init__(self, value): self.value = value self.defined = True class Nil(Option): def __init__(self): self.value = None self.defined = False nil = Nil()
Ruby——選項 Monad/Maybe Monad
class Monad # pure :: a -> M a def self.pure(x) raise StandardError("pure method needs to be implemented") end # pure :: a -> M a def pure(x) self.class.pure(x) end def flat_map(f) raise StandardError("flat_map method needs to be implemented") end # map :: # M a -> (a -> b) -> M b def map(f) flat_map(-> (x) { pure(f.call(x)) }) end end class Option < Monad attr_accessor :defined, :value # pure :: a -> Option a def self.pure(x) Some.new(x) end # pure :: a -> Option a def pure(x) Some.new(x) end # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(f) if defined f.call(value) else $none end end end class Some < Option def initialize(value) @value = value @defined = true end end class None < Option def initialize() @defined = false end end $none = None.new()
Swift——選項 Monad/Maybe Monad
import Foundation enum Maybe<A> { case None case Some(A) static func pure<B>(_ value: B) -> Maybe<B> { return .Some(value) } func flatMap<B>(_ f: (A) -> Maybe<B>) -> Maybe<B> { switch self { case .None: return .None case .Some(let value): return f(value) } } func map<B>(f: (A) -> B) -> Maybe<B> { return self.flatMap { type(of: self).pure(f($0)) } } }
Scala——選項 Monad/Maybe Monad
import language.higherKinds trait Monad[M[_]] { def pure[A](a: A): M[A] def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] def map[A, B](ma: M[A])(f: A => B): M[B] = flatMap(ma)(x => pure(f(x))) } object Monad { def apply[F[_]](implicit M: Monad[F]): Monad[F] = M implicit val myOptionMonad = new Monad[MyOption] { def pure[A](a: A) = MySome(a) def flatMap[A, B](ma: MyOption[A])(f: A => MyOption[B]): MyOption[B] = ma match { case MyNone => MyNone case MySome(a) => f(a) } } } sealed trait MyOption[+A] { def flatMap[B](f: A => MyOption[B]): MyOption[B] = Monad[MyOption].flatMap(this)(f) def map[B](f: A => B): MyOption[B] = Monad[MyOption].map(this)(f) } case object MyNone extends MyOption[Nothing] case class MySome[A](x: A) extends MyOption[A]
我們首先實現一個Monad
類,它將成為我們所有 monad 實現的基礎。 擁有這個類非常方便,因為只為特定的 monad 實現它的兩個方法pure
和flatMap
,你將免費獲得許多方法(我們在示例中將它們限制為簡單的map
方法,但通常有很多其他有用的方法,例如處理Monad
數組的sequence
和traverse
)。
我們可以map
表示為pure
和flatMap
的組合。 你可以從flatMap
的簽名 $flatMap: (T \to M[U]) \to (M[T] \to M[U])$ 中看出它真的很接近 $map: (T \to U) \到 (M[T] \到 M[U])$。 不同的是中間多了一個$M$,但是我們可以用pure
函數把$U$轉換成$M[U]$。 這樣我們就可以用flatMap
和pure
來表示map
。
這對 Scala 很有效,因為它有一個先進的類型系統。 它也適用於 JS、Python 和 Ruby,因為它們是動態類型的。 不幸的是,它不適用於 Swift,因為它是靜態類型的,並且沒有高級類型等高級類型特性,所以對於 Swift,我們必須為每個 monad 實現map
。
另請注意,Option monad 已經是 Swift 和 Scala 等語言的事實上的標準,因此我們為 monad 實現使用稍微不同的名稱。
現在我們有了一個基本的Monad
類,讓我們來看看我們的 Option monad 實現。 如前所述,基本思想是 Option 要么持有某個值(稱為Some
),要么根本不持有任何值( None
)。
pure
方法只是將一個值提升為Some
,而flatMap
方法檢查Option
的當前值——如果它是None
則返回None
,如果它是Some
具有底層 value ,它提取底層值,將f()
應用於它並返回一個結果。
請注意,僅使用這兩個函數和map
,永遠不可能陷入空指針異常。 (問題可能出現在我們的flatMap
方法的實現中,但這只是我們檢查一次的代碼中的幾行。之後,我們只需在數千個地方的整個代碼中使用我們的 Option monad 實現,而不是必須完全擔心空指針異常。)
要么單子
讓我們深入研究第二個 monad:Ether。 這與 Option monad 基本相同,但Some
稱為Right
, None
稱為Left
。 但這一次, Left
也被允許有一個潛在的價值。
我們需要它,因為表達拋出異常非常方便。 如果發生異常,則Either
的值為Left(Exception)
。 如果值為Left
,則flatMap
函數不會繼續執行,這重複了拋出異常的語義:如果發生異常,我們將停止進一步執行。
JavaScript——單子
import Monad from './monad'; export class Either extends Monad { // pure :: a -> Either a pure = (value) => { return new Right(value) } // flatMap :: # Either a -> (a -> Either b) -> Either b flatMap = f => this.isLeft() ? this : f(this.value) isLeft = () => this.constructor.name === 'Left' } export class Left extends Either { constructor(value) { super(); this.value = value; } toString() { return `Left(${this.value})` } } export class Right extends Either { constructor(value) { super(); this.value = value; } toString() { return `Right(${this.value})` } } // attempt :: (() -> a) -> M a Either.attempt = f => { try { return new Right(f()) } catch(e) { return new Left(e) } } Either.pure = (new Left(null)).pure
Python——單子
from monad import Monad class Either(Monad): # pure :: a -> Either a @staticmethod def pure(value): return Right(value) # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(self, f): if self.is_left: return self else: return f(self.value) class Left(Either): def __init__(self, value): self.value = value self.is_left = True class Right(Either): def __init__(self, value): self.value = value self.is_left = False
Ruby——單子
require_relative './monad' class Either < Monad attr_accessor :is_left, :value # pure :: a -> Either a def self.pure(value) Right.new(value) end # pure :: a -> Either a def pure(value) self.class.pure(value) end # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(f) if is_left self else f.call(value) end end end class Left < Either def initialize(value) @value = value @is_left = true end end class Right < Either def initialize(value) @value = value @is_left = false end end
Swift——單子
import Foundation enum Either<A, B> { case Left(A) case Right(B) static func pure<C>(_ value: C) -> Either<A, C> { return Either<A, C>.Right(value) } func flatMap<D>(_ f: (B) -> Either<A, D>) -> Either<A, D> { switch self { case .Left(let x): return Either<A, D>.Left(x) case .Right(let x): return f(x) } } func map<C>(f: (B) -> C) -> Either<A, C> { return self.flatMap { Either<A, C>.pure(f($0)) } } }
Scala——單子
package monad sealed trait MyEither[+E, +A] { def flatMap[EE >: E, B](f: A => MyEither[EE, B]): MyEither[EE, B] = Monad[MyEither[EE, ?]].flatMap(this)(f) def map[EE >: E, B](f: A => B): MyEither[EE, B] = Monad[MyEither[EE, ?]].map(this)(f) } case class MyLeft[E](e: E) extends MyEither[E, Nothing] case class MyRight[A](a: A) extends MyEither[Nothing, A] // ... implicit def myEitherMonad[E] = new Monad[MyEither[E, ?]] { def pure[A](a: A) = MyRight(a) def flatMap[A, B](ma: MyEither[E, A])(f: A => MyEither[E, B]): MyEither[E, B] = ma match { case MyLeft(a) => MyLeft(a) case MyRight(b) => f(b) } }
另請注意,捕獲異常很容易:您所要做的就是將Left
映射到Right
。 (雖然,為簡潔起見,我們在示例中沒有這樣做。)
未來的單子
讓我們探索我們需要的最後一個 monad:Future monad。 Future monad 基本上是一個容器,用於存儲現在可用或將在不久的將來可用的值。 您可以使用map
和flatMap
創建 Future 鏈,等待 Future 值被解析,然後再執行取決於首先解析的值的下一段代碼。 這與 JS 中的 Promises 概念非常相似。
我們現在的設計目標是將不同語言的現有異步 API 連接到一個一致的基礎。 事實證明,最簡單的設計方法是在 $constructor$ 中使用回調。
雖然回調設計在 JavaScript 和其他語言中引入了回調地獄問題,但對我們來說這不是問題,因為我們使用 monad。 事實上, Promise
對象——JavaScript 解決回調地獄的基礎——本身就是一個 monad!
那麼 Future monad 的構造函數呢? 是有這個簽名:
constructor :: ((Either err a -> void) -> void) -> Future (Either err a)

讓我們把它分成幾塊。 首先,讓我們定義:
type Callback = Either err a -> void
所以Callback
是一個函數,它接受錯誤或解析值作為參數,並且不返回任何內容。 現在我們的簽名看起來像這樣:
constructor :: (Callback -> void) -> Future (Either err a)
因此,我們需要為其提供一個不返回任何內容並在異步計算被解析為錯誤或某個值時觸發回調的函數。 看起來很容易為任何語言搭建橋樑。
至於 Future monad 本身的設計,我們來看看它的內部結構。 關鍵思想是有一個緩存變量,如果 Future monad 被解析,它會保存一個值,否則什麼都不保存。 您可以使用回調訂閱 Future,如果值被解析,它將立即觸發,否則,會將回調放入訂閱者列表。
一旦 Future 被解析,這個列表中的每個回調將在一個單獨的線程中使用解析的值被觸發一次(或者在 JS 的情況下作為事件循環中要執行的下一個函數)。請注意,至關重要的是謹慎使用同步原語,否則可能出現競爭條件。
基本流程是:您啟動作為構造函數參數提供的異步計算,並將其回調指向我們的內部回調方法。 同時,您可以訂閱 Future monad 並將您的回調放入隊列中。 計算完成後,內部回調方法調用隊列中的所有回調。 如果您熟悉反應式擴展(RxJS、RxSwift 等),它們使用非常相似的異步處理方法。
Future monad 的公共 API 由pure
、 map
和flatMap
組成,就像之前的 monad 一樣。 我們還需要幾個方便的方法:
-
async
,它採用同步阻塞函數並在單獨的線程上執行它,並且 traverse
,它接受一個值數組和將一個值映射到Future
的函數,並返回一個解析值數組的Future
讓我們看看結果如何:
JavaScript——未來的 Monad
import Monad from './monad'; import { Either, Left, Right } from './either'; import { none, Some } from './option'; export class Future extends Monad { // constructor :: ((Either err a -> void) -> void) -> Future (Either err a) constructor(f) { super(); this.subscribers = []; this.cache = none; f(this.callback) } // callback :: Either err a -> void callback = (value) => { this.cache = new Some(value) while (this.subscribers.length) { const subscriber = this.subscribers.shift(); subscriber(value) } } // subscribe :: (Either err a -> void) -> void subscribe = (subscriber) => (this.cache === none ? this.subscribers.push(subscriber) : subscriber(this.cache.value)) toPromise = () => new Promise( (resolve, reject) => this.subscribe(val => val.isLeft() ? reject(val.value) : resolve(val.value)) ) // pure :: a -> Future a pure = Future.pure // flatMap :: (a -> Future b) -> Future b flatMap = f => new Future( cb => this.subscribe(value => value.isLeft() ? cb(value) : f(value.value).subscribe(cb)) ) } Future.async = (nodeFunction, ...args) => { return new Future(cb => nodeFunction(...args, (err, data) => err ? cb(new Left(err)) : cb(new Right(data))) ); } Future.pure = value => new Future(cb => cb(Either.pure(value))) // traverse :: [a] -> (a -> Future b) -> Future [b] Future.traverse = list => f => list.reduce( (acc, elem) => acc.flatMap(values => f(elem).map(value => [...values, value])), Future.pure([]) )
Python——未來的 Monad
from monad import Monad from option import nil, Some from either import Either, Left, Right from functools import reduce import threading class Future(Monad): # __init__ :: ((Either err a -> void) -> void) -> Future (Either err a) def __init__(self, f): self.subscribers = [] self.cache = nil self.semaphore = threading.BoundedSemaphore(1) f(self.callback) # pure :: a -> Future a @staticmethod def pure(value): return Future(lambda cb: cb(Either.pure(value))) def exec(f, cb): try: data = f() cb(Right(data)) except Exception as err: cb(Left(err)) def exec_on_thread(f, cb): t = threading.Thread(target=Future.exec, args=[f, cb]) t.start() def async(f): return Future(lambda cb: Future.exec_on_thread(f, cb)) # flat_map :: (a -> Future b) -> Future b def flat_map(self, f): return Future( lambda cb: self.subscribe( lambda value: cb(value) if (value.is_left) else f(value.value).subscribe(cb) ) ) # traverse :: [a] -> (a -> Future b) -> Future [b] def traverse(arr): return lambda f: reduce( lambda acc, elem: acc.flat_map( lambda values: f(elem).map( lambda value: values + [value] ) ), arr, Future.pure([])) # callback :: Either err a -> void def callback(self, value): self.semaphore.acquire() self.cache = Some(value) while (len(self.subscribers) > 0): sub = self.subscribers.pop(0) t = threading.Thread(target=sub, args=[value]) t.start() self.semaphore.release() # subscribe :: (Either err a -> void) -> void def subscribe(self, subscriber): self.semaphore.acquire() if (self.cache.defined): self.semaphore.release() subscriber(self.cache.value) else: self.subscribers.append(subscriber) self.semaphore.release()
Ruby——未來的 Monad
require_relative './monad' require_relative './either' require_relative './option' class Future < Monad attr_accessor :subscribers, :cache, :semaphore # initialize :: ((Either err a -> void) -> void) -> Future (Either err a) def initialize(f) @subscribers = [] @cache = $none @semaphore = Queue.new @semaphore.push(nil) f.call(method(:callback)) end # pure :: a -> Future a def self.pure(value) Future.new(-> (cb) { cb.call(Either.pure(value)) }) end def self.async(f, *args) Future.new(-> (cb) { Thread.new { begin cb.call(Right.new(f.call(*args))) rescue => e cb.call(Left.new(e)) end } }) end # pure :: a -> Future a def pure(value) self.class.pure(value) end # flat_map :: (a -> Future b) -> Future b def flat_map(f) Future.new(-> (cb) { subscribe(-> (value) { if (value.is_left) cb.call(value) else f.call(value.value).subscribe(cb) end }) }) end # traverse :: [a] -> (a -> Future b) -> Future [b] def self.traverse(arr, f) arr.reduce(Future.pure([])) do |acc, elem| acc.flat_map(-> (values) { f.call(elem).map(-> (value) { values + [value] }) }) end end # callback :: Either err a -> void def callback(value) semaphore.pop self.cache = Some.new(value) while (subscribers.count > 0) sub = self.subscribers.shift Thread.new { sub.call(value) } end semaphore.push(nil) end # subscribe :: (Either err a -> void) -> void def subscribe(subscriber) semaphore.pop if (self.cache.defined) semaphore.push(nil) subscriber.call(cache.value) else self.subscribers.push(subscriber) semaphore.push(nil) end end end
Swift——未來的 Monad
import Foundation let background = DispatchQueue(label: "background", attributes: .concurrent) class Future<Err, A> { typealias Callback = (Either<Err, A>) -> Void var subscribers: Array<Callback> = Array<Callback>() var cache: Maybe<Either<Err, A>> = .None var semaphore = DispatchSemaphore(value: 1) lazy var callback: Callback = { value in self.semaphore.wait() self.cache = .Some(value) while (self.subscribers.count > 0) { let subscriber = self.subscribers.popLast() background.async { subscriber?(value) } } self.semaphore.signal() } init(_ f: @escaping (@escaping Callback) -> Void) { f(self.callback) } func subscribe(_ cb: @escaping Callback) { self.semaphore.wait() switch cache { case .None: subscribers.append(cb) self.semaphore.signal() case .Some(let value): self.semaphore.signal() cb(value) } } static func pure<B>(_ value: B) -> Future<Err, B> { return Future<Err, B> { $0(Either<Err, B>.pure(value)) } } func flatMap<B>(_ f: @escaping (A) -> Future<Err, B>) -> Future<Err, B> { return Future<Err, B> { [weak self] cb in guard let this = self else { return } this.subscribe { value in switch value { case .Left(let err): cb(Either<Err, B>.Left(err)) case .Right(let x): f(x).subscribe(cb) } } } } func map<B>(_ f: @escaping (A) -> B) -> Future<Err, B> { return self.flatMap { Future<Err, B>.pure(f($0)) } } static func traverse<B>(_ list: Array<A>, _ f: @escaping (A) -> Future<Err, B>) -> Future<Err, Array<B>> { return list.reduce(Future<Err, Array<B>>.pure(Array<B>())) { (acc: Future<Err, Array<B>>, elem: A) in return acc.flatMap { elems in return f(elem).map { val in return elems + [val] } } } } }
Scala——未來的 Monad
package monad import java.util.concurrent.Semaphore class MyFuture[A] { private var subscribers: List[MyEither[Exception, A] => Unit] = List() private var cache: MyOption[MyEither[Exception, A]] = MyNone private val semaphore = new Semaphore(1) def this(f: (MyEither[Exception, A] => Unit) => Unit) { this() f(this.callback _) } def flatMap[B](f: A => MyFuture[B]): MyFuture[B] = Monad[MyFuture].flatMap(this)(f) def map[B](f: A => B): MyFuture[B] = Monad[MyFuture].map(this)(f) def callback(value: MyEither[Exception, A]): Unit = { semaphore.acquire cache = MySome(value) subscribers.foreach { sub => val t = new Thread( new Runnable { def run: Unit = { sub(value) } } ) t.start } subscribers = List() semaphore.release } def subscribe(sub: MyEither[Exception, A] => Unit): Unit = { semaphore.acquire cache match { case MyNone => subscribers = sub :: subscribers semaphore.release case MySome(value) => semaphore.release sub(value) } } } object MyFuture { def async[B, C](f: B => C, arg: B): MyFuture[C] = new MyFuture[C]({ cb => val t = new Thread( new Runnable { def run: Unit = { try { cb(MyRight(f(arg))) } catch { case e: Exception => cb(MyLeft(e)) } } } ) t.start }) def traverse[A, B](list: List[A])(f: A => MyFuture[B]): MyFuture[List[B]] = { list.foldRight(Monad[MyFuture].pure(List[B]())) { (elem, acc) => Monad[MyFuture].flatMap(acc) ({ values => Monad[MyFuture].map(f(elem)) { value => value :: values } }) } } } // ... implicit val myFutureMonad = new Monad[MyFuture] { def pure[A](a: A): MyFuture[A] = new MyFuture[A]({ cb => cb(myEitherMonad[Exception].pure(a)) }) def flatMap[A, B](ma: MyFuture[A])(f: A => MyFuture[B]): MyFuture[B] = new MyFuture[B]({ cb => ma.subscribe(_ match { case MyLeft(e) => cb(MyLeft(e)) case MyRight(a) => f(a).subscribe(cb) }) }) }
現在,請注意Future
的公共 API 如何不包含任何低級細節,如線程、信號量或任何此類內容。 你所需要的基本上就是提供帶有回調的東西,就是這樣!
從 Monad 編寫程序
好的,讓我們嘗試使用我們的 monad 來製作一個實際的程序。 假設我們有一個包含 URL 列表的文件,並且我們希望並行獲取這些 URL 中的每一個。 然後,為了簡潔起見,我們希望將響應減少到每個 200 字節並打印出結果。
我們首先將現有的語言 API 轉換為單子接口(參見函數readFile
和fetch
)。 現在我們有了它,我們可以將它們組合起來以獲得最終結果作為一個鏈。 請注意,鏈本身是超級安全的,因為所有血淋淋的細節都包含在單子中。
JavaScript——示例 Monad 程序
import { Future } from './future'; import { Either, Left, Right } from './either'; import { readFile } from 'fs'; import https from 'https'; const getResponse = url => new Future(cb => https.get(url, res => { var body = ''; res.on('data', data => body += data); res.on('end', data => cb(new Right(body))); res.on('error', err => cb(new Left(err))) })) const getShortResponse = url => getResponse(url).map(resp => resp.substring(0, 200)) Future .async(readFile, 'resources/urls.txt') .map(data => data.toString().split("\n")) .flatMap(urls => Future.traverse(urls)(getShortResponse)) .map(console.log)
Python—Sample Monad Program
import http.client import threading import time import os from future import Future from either import Either, Left, Right conn = http.client.HTTPSConnection("en.wikipedia.org") def read_file_sync(uri): base_dir = os.path.dirname(__file__) #<-- absolute dir the script is in path = os.path.join(base_dir, uri) with open(path) as f: return f.read() def fetch_sync(uri): conn.request("GET", uri) r = conn.getresponse() return r.read().decode("utf-8")[:200] def read_file(uri): return Future.async(lambda: read_file_sync(uri)) def fetch(uri): return Future.async(lambda: fetch_sync(uri)) def main(args=None): lines = read_file("../resources/urls.txt").map(lambda res: res.splitlines()) content = lines.flat_map(lambda urls: Future.traverse(urls)(fetch)) output = content.map(lambda res: print("\n".join(res))) if __name__ == "__main__": main()
Ruby—Sample Monad Program
require './lib/future' require 'net/http' require 'uri' semaphore = Queue.new def read(uri) Future.async(-> () { File.read(uri) }) end def fetch(url) Future.async(-> () { uri = URI(url) Net::HTTP.get_response(uri).body[0..200] }) end read("resources/urls.txt") .map(-> (x) { x.split("\n") }) .flat_map(-> (urls) { Future.traverse(urls, -> (url) { fetch(url) }) }) .map(-> (res) { puts res; semaphore.push(true) }) semaphore.pop
Swift—Sample Monad Program
import Foundation enum Err: Error { case Some(String) } func readFile(_ path: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(fileURLWithPath: path) let text = try? String(contentsOf: url) if let res = text { callback(Either<Error, String>.pure(res)) } else { callback(Either<Error, String>.Left(Err.Some("Error reading urls.txt"))) } } } } func fetchUrl(_ url: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(string: url) let task = URLSession.shared.dataTask(with: url!) {(data, response, error) in if let err = error { callback(Either<Error, String>.Left(err)) return } guard let nonEmptyData = data else { callback(Either<Error, String>.Left(Err.Some("Empty response"))) return } guard let result = String(data: nonEmptyData, encoding: String.Encoding.utf8) else { callback(Either<Error, String>.Left(Err.Some("Cannot decode response"))) return } let index = result.index(result.startIndex, offsetBy: 200) callback(Either<Error, String>.pure(String(result[..<index]))) } task.resume() } } } var result: Any = "" let _ = readFile("\(projectDir)/Resources/urls.txt") .map { data -> [String] in data.components(separatedBy: "\n").filter { (line: String) in !line.isEmpty } }.flatMap { urls in return Future<Error, String>.traverse(urls) { url in return fetchUrl(url) } }.map { responses in print(responses) } RunLoop.main.run()
Scala—Sample Monad Program
import scala.io.Source import java.util.concurrent.Semaphore import monad._ object Main extends App { val semaphore = new Semaphore(0) def readFile(name: String): MyFuture[List[String]] = MyFuture.async[String, List[String]](filename => Source.fromResource(filename).getLines.toList, name) def fetch(url: String): MyFuture[String] = MyFuture.async[String, String]( uri => Source.fromURL(uri).mkString.substring(0, 200), url ) val future = for { urls <- readFile("urls.txt") entries <- MyFuture.traverse(urls)(fetch _) } yield { println(entries) semaphore.release } semaphore.acquire }
There you have it—monad solutions in practice. You can find a repo containing all the code from this article on GitHub.
Overhead: Done. Benefits: Ongoing
For this simple monad-based program, it might look like overkill to use all the code that we wrote before. But that's just the initial setup, and it will stay constant in its size. Imagine that from now on, using monads, you can write a lot of async code, not worrying about threads, race conditions, semaphores, exceptions, or null pointers! 驚人的!