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
具有基础值,它提取基础值,将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! 惊人的!