本文概述
- 范畴论导论
- 输入Monad
- Option Monad, 又称Maybe Monad
- JavaScript-Option Monad /Maybe Monad
- Python-Option Monad /Maybe Monad
- Ruby-Option Monad /Maybe Monad
- 迅捷—Option Monad /Maybe Monad
- Scala-Option Monad /Maybe Monad
- 莫纳德
- JavaScript — Monad之一
- Python — Monad之一
- Ruby-Monad之一
- 斯威夫特—莫纳德
- Scala-Monad之一
- Future Monad
- JavaScript-未来Monad
- Python-未来Monad
- Ruby-未来世界
- 斯威夫特-Future Monad
- Scala-未来Monad
- 用Monads编写程序
- JavaScript —示例Monad程序
- Python-示例Monad程序
- Ruby-示例Monad程序
- Swift-示例Monad程序
- Scala-Monad示例程序
- 开销:完成。好处:进行中
本monad教程简要说明了monad, 并展示了如何在五种不同的编程语言中实现最有用的-如果你正在寻找JavaScript中的monad, Python中的monad, Ruby中的monad, Swift中的monad和/或monad。在Scala中, 或者要比较任何实现, 你正在阅读正确的文章!
使用这些monad, 你将摆脱一系列错误, 例如空指针异常, 未处理的异常和竞争条件。
这是我在下面介绍的内容:
- 范畴论导论
- Monad的定义
- Option(“也许”)monad, Either monad和Future monad的实现, 以及利用它们的示例程序在JavaScript, Python, Ruby, Swift和Scala中
让我们开始吧!我们的第一站是范畴论, 这是Monad论的基础。
范畴论导论
范畴理论是在20世纪中叶积极发展的数学领域。现在, 它是包括monad在内的许多功能编程概念的基础。让我们快速浏览一些针对软件开发术语而调整的类别理论概念。
因此, 有三个定义类别的核心概念:
- 类型就像我们在静态类型语言中看到的那样。示例:整数, 字符串, 狗, 猫等。
- 函数连接两种类型。因此, 它们可以表示为从一种类型到另一种类型或它们自己的箭头。从$ 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 $ —那么你可以$ even {\ _} strlen函数:String \ to Boolean $, 它判断字符串的长度是否为偶数。在这种情况下, $ even {\ _} strlen =甚至\ 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和所有其他类型均保证为非null, 即null值不存在。
旁注2:实际上, 这只是类别的一部分, 但这只是我们讨论的全部内容, 因为它具有我们需要的所有基本组成部分, 并且该图的混乱程度也较小。真实类别还将具有所有组合函数, 例如$ roundToString:Double \ to String = intToString \ cdot round $, 以满足类别的composition子句。
你可能会注意到, 此类别中的功能非常简单。实际上, 在这些功能中存在错误几乎是不可能的。没有空值, 没有异常, 只有算术运算和使用内存。因此, 唯一可能发生的坏事是处理器或内存故障(在这种情况下, 你仍然需要使程序崩溃), 但是这种情况很少发生。
如果我们所有的代码都只在这种稳定性水平下工作, 那会很好吗?绝对!但是, 例如, I / O呢?我们绝对不能没有它。这就是monad解决方案的出路:它们将所有不稳定的操作隔离为非常小的且经过很好审计的代码段, 然后你就可以在整个应用程序中使用稳定的计算!
输入Monad
让我们将不稳定的行为(例如I / O)称为副作用。现在, 我们希望能够在存在这种副作用的情况下, 以稳定的方式使用我们之前定义的所有函数(例如长度和类型, 例如String)。
因此, 让我们从一个空类别$ M [A] $开始, 并使其成为一个类别, 该类别将具有带有一种特定类型的副作用的值以及没有副作用的值。假设我们已经定义了此类别, 并且该类别为空。目前, 我们对此无能为力, 因此, 为了使它有用, 我们将遵循以下三个步骤:
- 用类别$ A $中的类型的值填充它, 例如String, Int, Double等(下图中的绿色框)。
- 一旦有了这些值, 我们仍然无法对它们执行任何有意义的操作, 因此我们需要一种方法来将每个函数$ f:T \转换为$ A $中的U $并创建一个函数$ g:M [T] \ to M [U] $(下图中的蓝色箭头)。一旦有了这些功能, 我们就可以使用类别$ A [$]中的$ M [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.srcmini02.com/javascript/option-maybe-two-future-monads-js, 为此, 你需要创建一个函数$ fetch $。由于在获取内容时可能会出错(请考虑网络故障等), 因此你将$ M [String] $作为此函数的返回类型。因此, 它看起来类似于$ fetch:String \ to M [String] $, 在函数体中的某处, 我们将$ constructor $用于$ M $。
现在假设我们创建了一个模拟函数来测试:$ fetchMock:字符串\至M [String] $。它仍然具有相同的签名, 但是这次我们只是将结果HTML页面注入$ fetchMock $的主体中, 而没有进行任何不稳定的网络操作。因此, 在这种情况下, 我们只需在$ fetchMock $的实现中使用$ pure $。
下一步, 我们需要一个函数, 该函数可以安全地将类别$ A $中的任意函数$ f $提升为$ 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 $。假设我们要从srcmini02.com抓取HTML页面, 然后在其中扫描所有URL并获取它们。我将创建一个函数$ fetch:String \ to M [String] $, 该函数仅获取一个URL并返回HTML页面。
然后, 我将此功能应用于网址, 并从srcmini02.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] $。
现在, 我们已经完成了所有三个步骤, 我们实际上可以组成我们需要的任何值转换。例如, 如果你的值$ x $的类型为$ M [T] $和$ 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是OptionMonad。如果你使用的是经典编程语言, 则可能会因为臭名昭著的空指针错误而遇到很多崩溃。 null的发明者Tony Hoare将此发明称为”十亿美元的错误”:
这导致了无数的错误, 漏洞和系统崩溃, 在最近四十年中可能造成十亿美元的痛苦和破坏。
因此, 我们尝试对此进行改进。 Option monad持有一些非空值, 或者没有值。与null值非常相似, 但是有了这个monad, 我们可以安全地使用定义良好的函数, 而不必担心null指针异常。让我们看一下不同语言的实现:
JavaScript-Option 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-Option 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-Option 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()
迅捷—Option 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-Option 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数组的序列和遍历)。
我们可以将地图表示为pure和flatMap的组成。你可以从flatMap的签名$ flatMap中看到:(T \ to M [U])\ to(M [T] \ to M [U])$, 它确实接近$ map:(T \ to U)\ to( M [T] \ to M [U])$。区别在于中间有额外的$ M $, 但是我们可以使用pure函数将$ U $转换为$ M [U] $。这样, 我们就可以用flatMap和pure来表示地图。
这对Scala很好, 因为它具有高级类型系统。它对于JS, Python和Ruby也很有效, 因为它们是动态类型的。不幸的是, 它不适用于Swift, 因为它是静态类型的, 并且不具有高级类型的功能(例如类型较高的类型), 因此对于Swift, 我们必须为每个monad实现地图。
还要注意, Option monad已经是诸如Swift和Scala之类的语言的事实上的标准, 因此我们在monad实现中使用略有不同的名称。
现在我们有了基本的Monad类, 让我们开始我们的Option monad实现。如前所述, 基本思想是期权持有某些价值(称为”某”)或根本不持有任何价值(“无”)。
pure方法只是将值提升为Some, 而flatMap方法检查Option的当前值-如果它为None则返回None, 如果它为Some且具有基础值, 则提取基础值, 将f()应用于它并返回结果。
请注意, 仅使用这两个函数和映射, 就不可能陷入空指针异常。 (这个问题可能会在我们执行flatMap方法时出现, 但这只是我们代码中的几行, 我们只需要检查一下。在那之后, 我们仅在数千个地方的整个代码中使用Option monad实现, 而不必完全不必担心null指针异常。)
莫纳德
让我们深入了解第二个monad:。这基本上与Option单声道相同, 但是Some称为Right, None称为Left。但是这次, Left也被允许具有基础价值。
我们需要这样做是因为表达异常非常方便。如果发生异常, 则Either的值将为Left(Exception)。如果值为Left, 则flatMap函数不会继续执行, 该函数会重复抛出异常的语义:如果发生异常, 我们将停止进一步执行。
JavaScript — Monad之一
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 — Monad之一
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-Monad之一
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
斯威夫特—莫纳德
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-Monad之一
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)
}
}
另外请注意, 捕获异常很容易:你要做的就是从左到右映射。 (尽管如此, 为简洁起见, 我们在示例中并未这样做。)
Future Monad
让我们探索我们需要的最后一个monad :: Future monad。 Future monad基本上是一个值的容器, 该值现在可以使用, 也可以在不久的将来使用。你可以使用map和flatMap制作Future链, 它们将等待Future值解析, 然后再执行下一个取决于首先解析的值的代码。这与JS中Promises的概念非常相似。
我们现在的设计目标是将不同语言的现有异步API桥接到一个一致的基础上。事实证明, 最简单的设计方法是在$ constructor $中使用回调。
尽管回调设计在JavaScript和其他语言中引入了回调地狱问题, 但对我们而言这并不是问题, 因为我们使用monad。实际上, Promise对象(JavaScript解决回调地狱的基础)本身就是monad!
那么Future monad的构造函数呢?具有此签名:
构造函数::((err a-> void)-> void)-> Future(err a)
让我们将其分成几部分。首先, 让我们定义:
输入Callback = err a-> void
因此, 回调函数是一个将错误或解析值作为参数, 并且不返回任何内容的函数。现在, 我们的签名如下所示:
构造函数::(Callback-> void)-> Future(err a)
因此, 我们需要为其提供一个不返回任何内容并在异步计算解析为错误或某个值后立即触发回调的函数。看起来很容易为任何语言搭建桥梁。
至于Future monad本身的设计, 让我们看一下其内部结构。关键思想是拥有一个缓存变量, 该变量在解析FutureMonad之后将保存一个值, 否则将不保存任何值。你可以使用回调来订阅Future, 如果解析了值, 则会立即触发该回调, 否则将立即将该回调放入订户列表。
解析完Future之后, 此列表中的每个回调将在单独的线程中使用解析后的值精确触发一次(对于JS, 则作为事件循环中要执行的下一个函数。)请注意, 谨慎使用同步原语, 否则竞争条件是可能的。
基本流程是:启动作为构造函数参数提供的异步计算, 并将其回调指向我们的内部回调方法。同时, 你可以订阅Future monad并将回调放入队列。一旦计算完成, 内部回调方法将调用队列中的所有回调。如果你熟悉反应式扩展程序(RxJS, RxSwift等), 它们会使用非常相似的方法进行异步处理。
就像以前的Monad一样, FutureMonad的公共API由pure, map和flatMap组成。我们还需要几种方便的方法:
- 异步, 它具有同步阻止功能并在单独的线程上执行, 并且
- 遍历, 它采用值数组和将值映射到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-未来世界
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
斯威夫特-Future 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)
})
})
}
现在, 请注意, 未来的公共API如何不包含任何低级详细信息, 例如线程, 信号量或任何其他东西。你所需要做的基本上就是为回调提供一些东西, 仅此而已!
用Monads编写程序
好的, 让我们尝试使用我们的monad编写一个实际的程序。假设我们有一个包含URL列表的文件, 我们想并行获取每个URL。然后, 为了简洁起见, 我们希望将响应每个剪切为200个字节并打印出结果。
我们首先将现有的语言API转换为monadic接口(请参见函数readFile和fetch)。现在我们有了我们可以将它们组合起来以获得一个链的最终结果。请注意, 链条本身是超级安全的, 因为所有血腥细节都包含在monad中。
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-示例Monad程序
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-示例Monad程序
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-示例Monad程序
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-Monad示例程序
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
}
在那里, 你可以在实践中找到monad解决方案。你可以在GitHub上找到包含本文所有代码的仓库。
开销:完成。好处:进行中
对于这个简单的基于monad的程序, 使用我们之前编写的所有代码可能看起来有些矫kill过正。但这只是初始设置, 它的大小将保持不变。想象一下, 从现在开始, 你可以使用monad编写很多异步代码, 而不必担心线程, 竞争条件, 信号量, 异常或空指针!太棒了!
评论前必须登录!
注册