路径 clip-rule="evenodd" d="M33.377 4.574a3.508 3.508 0 0 0-2.633-1.126c-1 0-1.993.67-2.604 1.334l.002-1.24-1.867-.002-.02 10.17v.133l1.877.002.008-3.18c.567.611 1.464.97 2.462.973 1.099 0 2.022-.377 2.747-1.117.73-.745 1.1-1.796 1.103-3.002.003-1.232-.358-2.222-1.075-2.945Zm-3.082.55c.637 0 1.176.23 1.602.683.438.438.663 1.012.66 1.707-.003.7-.22 1.33-.668 1.787-.428.438-.964.661-1.601.661-.627 0-1.15-.22-1.6-.666-.445-.46-.662-1.086-.662-1.789.003-.695.227-1.27.668-1.708a2.13 2.13 0 0 1 1.596-.675h.005Zm5.109-.067-.008 4.291c-.002.926.263 1.587.784 1.963.325.235.738.354 1.228.354.376 0 .967-.146.967-.146l-.168-1.564s-.43.133-.64-.01c-.198-.136-.296-.428-.296-.866l.008-4.022 1.738.002.002-1.492-1.738-.002.005-2.144-1.874-.002-.005 2.143-1.573-.002 1.57 1.497ZM20.016 1.305h-9.245l-.002 1.777h3.695l-.016 8.295v.164l1.955.002-.008-8.459 3.621-.002V1.305Z" fill="#262D3D" fill-rule="evenodd">路径><路径 clip-rule="evenodd" d="M10.06 5.844 7.277 3.166 4.015.03 2.609 1.374l2.056 1.978-4.51 4.313 6.065 5.831 1.387-1.327-2.073-1.994 4.526-4.331ZM4.274 8.7a.211.211 0 0 1-.124 0c-.04-.013-.074-.03-.15-.102l-.817-.787c-.072-.069-.092-.104-.105-.143a.187.187 0 0 1 0-.12c.013-.039.03-.07.105-.143L5.76 4.938c.072-.07.108-.09.15-.099a.21.21 0 0 1 .123 0c.041.012.075.03.15.101L7 5.727c.072.07.093.104.103.144.013.04.013.08 0 .119-.013.04-.03.072-.106.143L4.422 8.601a.325.325 0 0 1-.147.099Z" fill="#204ECF" fill-rule="evenodd">路径><路径 clip-rule="evenodd" d="M24.354 4.622a3.94 3.94 0 0 0-2.876-1.149 4.1 4.1 0 0 0-2.829 1.084c-.804.725-1.214 1.733-1.217 2.992-.002 1.26.405 2.267 1.207 2.995a4.114 4.114 0 0 0 2.832 1.094c.04.002.082.002.123.002a3.967 3.967 0 0 0 2.75-1.138c.538-.532 1.183-1.473 1.186-2.938.002-1.465-.637-2.408-1.176-2.942Zm-.59 2.94c-.003.73-.228 1.334-.671 1.794-.441.458-.99.69-1.633.69a2.166 2.166 0 0 1-1.614-.697c-.43-.45-.65-1.057-.65-1.797s.222-1.344.655-1.795a2.17 2.17 0 0 1 1.617-.69c.64 0 1.189.235 1.63.698.443.46.668 1.064.665 1.797ZM41.15 6.324c0-.458.25-1.465 1.632-1.465.49 0 .768.159 1.003.347.227.18.34.626.34.994v.174l-2.282.341C40.035 6.98 39 7.913 38.993 9.28c-.002.708.266 1.314.777 1.76.503.438 1.191.67 2.004.673 1.023 0 1.792-.354 2.341-1.084.003.31.003.621.003.91h1.903l.013-5.246c.002-.856-.289-1.685-.864-2.14-.567-.449-1.31-.679-2.386-.681h-.015c-.82 0-1.69.208-2.274.695-.689.572-1.027 1.478-1.027 2.178l1.682-.02Zm.864 3.814c-.676-.002-1.115-.371-1.112-.938.003-.589.43-.933 1.346-1.081l1.875-.305v.017c-.005 1.36-.87 2.307-2.102 2.307h-.008Zm4.917-8.712-.018 10.058v.044l1.684.005.018-10.06v-.045l-1.684-.002Zm2.654 9.491c0-.173.062-.322.19-.445a.645.645 0 0 1 .462-.186c.18 0 .338.062.465.186a.596.596 0 0 1 .193.445.583.583 0 0 1-.193.443.644.644 0 0 1-.465.183.634.634 0 0 1-.461-.183.59.59 0 0 1-.191-.443Zm.108 0c0 .146.052.273.158.376a.54.54 0 0 0 .389.154.539.539 0 0 0 .547-.53.498.498 0 0 0-.16-.373.531.531 0 0 0-.387-.156.531.531 0 0 0-.387.155.497.497 0 0 0-.16.374Zm.702.344-.176-.3h-.118v.3h-.109v-.688h.292c.144 0 .23.082.23.196 0 .096-.076.168-.176.188l.178.304h-.121Zm-.294-.596v.21h.167c.093 0 .14-.034.14-.104 0-.072-.047-.106-.14-.106h-.167Z" fill="#262D3D" fill-rule="evenodd">路径>作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Alexey (MEcon)精通几种语言,更喜欢函数式编程, 尤其是Scala, 为了减少浪费在捉虫子上的时间.
本monad教程简要介绍了monad,并展示了如何在五种不同的编程语言中实现最有用的monad——如果您正在寻找monad JavaScript,单子在 Python,单子在 Ruby,单子在 斯威夫特,和/或单子 Scala,或者比较任何实现,您正在阅读正确的文章!
使用这些单子,你将摆脱一系列的错误,如空指针异常, 未处理的异常, 以及竞态条件.
以下是我要介绍的内容:
让我们开始吧! 我们的第一站是范畴论,它是单子的基础.
范畴论是20世纪中叶蓬勃发展起来的一个数学领域. 现在它是包括单子在内的许多函数式编程概念的基础. 让我们快速看一下范畴论的一些概念, 针对软件开发术语进行了调优.
定义a有三个核心概念 类别:
Int
, 字符串
, 狗
, 猫
等.字符串
甚至. 在这种情况下$even{\_}strlen = even \cdot strlen$. 构成包含两个特征:
我们来看一个简单的分类.
旁注:我们假设 Int
, 字符串
所有其他类型都保证非空I.e.,空值不存在.
旁注2:这实际上只是 部分 属于某一类的, 但这就是我们讨论的全部内容, 因为它有我们需要的所有基本部分,而且这样的图表不那么混乱. 真正的类别也将具有所有组合函数,如$roundTo字符串: 双 \to 字符串 = intTo字符串 \cdot round$, 满足范畴的构成子句.
您可能会注意到,这类函数非常简单. 事实上,这些函数几乎不可能有bug. 没有空,没有异常,只有算术和内存处理. 因此,唯一可能发生的糟糕情况是处理器或内存故障——在这种情况下,无论如何都需要使程序崩溃——但这种情况很少发生.
如果我们所有的代码都能在这样的稳定水平上工作,那不是很好吗? 绝对! 但是举例来说,I/O呢? 我们绝对不能没有它. 这就是monad解决方案的拯救之处:它们将所有不稳定的操作隔离到超小且经过良好审计的代码片段中,然后您可以在整个应用程序中使用稳定的计算!
我们把不稳定的行为称为I/O a 副作用. 现在我们希望能够处理所有之前定义的函数,比如 长度
像这样的类型 字符串
在这种情况下以稳定的方式 副作用.
因此,让我们从一个空的类别$M[A]$开始,并将其转换为一个类别,该类别将具有具有特定类型副作用的值,也将具有没有副作用的值. 假设我们已经定义了这个类别,并且它是空的. 现在我们对它无能为力, 为了使它有用, 我们将遵循以下三个步骤:
字符串
, Int
, 双
等. (下图中的绿色方框)因此,让我们从定义两种将$A$类型的值提升到$M[A]$类型的值的方法开始:一种函数没有副作用,另一种函数有副作用.
字符串
. 在这种情况下,构造函数美元返回的值类型为$M[字符串]$.现在我们有两种方法将价值提升到$M[A]$, 作为程序员,您可以选择使用哪个函数, 这取决于你的项目目标. 让我们考虑一个示例:您想获取一个HTML页面,如http://www.toptal.Com/javascript/option-maybe-either-future-monads-js,为此你创建一个函数$获取$. 因为在获取数据时任何事情都可能出错——想想网络故障等等.-您将使用$M[字符串]$作为此函数的返回类型. 所以它看起来就像$获取: 字符串 \to M[字符串]$在函数体的某个地方,我们将使用构造函数美元 for $M$.
现在让我们假设我们创建了一个用于测试的模拟函数:$获取Mock: 字符串 \to M[字符串]$. 它仍然有相同的签名, 但是这次我们只是将生成的HTML页面注入到$获取Mock$的主体中,而不做任何不稳定的网络操作. 所以在这种情况下,我们只是在$获取Mock$的实现中使用$纯$.
作为下一步, 我们需要一个函数,可以安全地将任意函数从类别$ a $提升到$M[a]$(图中的蓝色箭头). 这个函数叫做$map: (T \to U) \to (M[T] \to M[U])$.
现在我们有了一个类别(如果我们使用$构造函数,它可能会产生副作用), 它也有稳定类的所有函数, 这意味着它们在M[A]$中也很稳定. 您可能注意到,我们显式地引入了另一类函数,如$f: T \to M[U]$. E.g., $纯$和构造函数美元就是用于$U = T$的此类函数的例子, 但显然还有更多, 例如,如果我们使用$纯$,然后使用$map$. 一般来说,我们需要一种方法来处理任意形式的函数f: T \to M[U]$.
如果我们想创建一个基于f的新函数,它可以应用于M[T], 我们可以尝试使用$map$. 这就引出函数$g: M[T] \到M[M[U]]$, 这是不好的,因为我们不想有更多的类别$M[M[A]]$. 来处理这个问题, 我们引入最后一个函数:$flatMap: (T \to M[U]) \to (M[T] \to M[U])$.
但我们为什么要这么做呢? 假设我们在第二步i之后.e.,我们有$纯$、构造函数美元和$map$. 假设我们想从total抓取一个HTML页面.com,然后扫描那里的所有url并获取它们. 我会做一个函数$获取: 字符串 \to M[字符串]$,它只获取一个URL并返回一个HTML页面.
然后,我将此函数应用于URL并从total获取页面.$x: M[字符串]$. 现在,我对$x$做一些转换,最后得到某个URL $u: M[字符串]$. 我想对它应用函数$获取$,但是我不能,因为它的类型是$字符串$,而不是$M[字符串]$. 这就是为什么我们需要$flatMap$来转换$获取: 字符串 \to M[字符串]$到$m_获取: M[字符串] \to M[字符串]$.
现在我们已经完成了这三个步骤, 实际上,我们可以组合任何需要的值转换. 例如, 如果您的值$x$的类型为$M[T]$和$f: T \到$ U$, 您可以使用$map$将$f$应用于值$x$,并获得类型$M[U]$的值$y$. 这样,任何值的转换都可以以100%无bug的方式完成, 只要$纯$, 构造函数美元, $map$和$flatMap$实现是无bug的.
因此,与其每次在代码库中遇到一些令人讨厌的效果,不如处理它们, 您只需要确保正确实现了这四个函数. 在节目的最后, 您将只得到一个$M[X]$,您可以安全地打开值$X$并处理所有错误情况.
这就是monad:一个实现$纯$、$map$和$flatMap$的东西. (实际上,$map$可以由$纯$和$flatMap$派生而来, 但它是非常有用和广泛的功能, 所以我没有从定义中省略它.)
好了,让我们深入了解单子的实际实现和使用. 第一个真正有用的单子是Option单子. 如果你是从经典编程语言学来的, 由于臭名昭著的空指针错误,您可能遇到过许多崩溃. null的发明者Tony Hoare称这项发明为“十亿美元的错误”:
这导致了无数的错误, 漏洞, 系统崩溃了, 在过去的四十年里,它们可能造成了十亿美元的痛苦和损害.
让我们试着改进一下. Option单子要么保存一些非空值,要么没有值. 非常类似于空值, 但是有了这个单子, 我们可以安全地使用定义良好的函数,而不必担心空指针异常. 让我们看一下不同语言的实现:
类单孢体 {
// 纯 :: a -> M a
纯 = () => { throw "纯 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 => 这.flatMap(x => 新 这.纯(f (x)))
}
导出类选项扩展单孢体 {
// 纯 :: a -> Option a
纯 = (值) => {
If ((value === null) || (value === un定义)) {
返回所有;
}
返回新 一些(值)
}
// flatMap :: # Option a -> (a -> Option b) -> Option b
flatMap = f =>
这.构造函数.name === '无' ?
没有:
f(这.值)
// equals :: # M a -> M a -> boolean
equals = (x) => 这.to字符串 () === x.to字符串 ()
}
类没有一个扩展选项{
to字符串 () {
返回“没有”;
}
}
//缓存无类值
export const none = 新 none ()
Option.Pure =无.纯
导出类扩展选项{
构造函数(值){
超级();
这.Value = Value;
}
to字符串 () {
返回一些($ {.值})”
}
}
类单子:
# 纯 :: a -> M a
@staticmethod
def纯(x):
raise Exception("纯 method需要被实现")
# flat_map :: # M a -> (a -> M b) -> M b
Def flat_map (自我, f):
引发异常("flat_map方法需要被实现")
# map :: # M a -> (a -> b) -> M b
Def 地图(自我, f):
回归自我.Flat_map (lambda x: 自我.纯(f (x)))
类选项(单轴):
# 纯 :: a -> Option a
@staticmethod
def纯(x):
返回一些(x)
# flat_map :: # Option a -> (a -> Option b) -> Option b
Def flat_map (自我, f):
如果自我.定义:
返回f(自我.值)
其他:
返回nil
类(选择):
Def __init__(自我, 值):
自我.Value = Value
自我.defined = True
类Nil(选项):
def __init__(自我):
自我.value =无
自我.defined = False
nil = nil ()
类的单子
# 纯 :: a -> M a
def自我.纯(x)
引发st和derror ("纯 method需要被实现")
结束
# 纯 :: a -> M a
def纯(x)
自我.class.纯(x)
结束
def flat_map (f)
引发st和derror ("flat_map方法需要被实现")
结束
# map :: # M a -> (a -> b) -> M b
def地图(f)
flat_map (-> (x) { 纯(f.调用(x))})
结束
结束
class Option < 单孢体
Attr_accessor:定义,:value
# 纯 :: a -> Option a
def自我.纯(x)
一些.新(x)
结束
# 纯 :: a -> Option a
def纯(x)
一些.新(x)
结束
# flat_map :: # Option a -> (a -> Option b) -> Option b
def flat_map (f)
如果定义
f.调用(值)
其他的
没有美元
结束
结束
结束
class 一些 < Option
def初始化(值)
@Value = Value
@defined = true
结束
结束
class 没有一个 < Option
def初始化()
@defined = false
结束
结束
没有美元 = none.新()
导入的语言.higherKinds
性状单子[M[_]] {
def 纯[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 => 纯(f (x)))
}
对象单子{
def apply[F[_]](隐式M: 单孢体[F]): 单孢体[F] = M
隐式val myOption单孢体 =新的单子(MyOption) {
def纯 [A](A: A) = My一些(A)
def flatMap[A, B](ma: MyOption[A])(f: A => MyOption[B]): MyOption[B] = ma match {
情况下 My没有一个 => My没有一个
情况下 My一些(a) => f(a)
}
}
}
MyOption[+A] {
def flatMap[B](f: A => MyOption[B]): MyOption[B] =
单子(MyOption).flatMap(这)(f)
def map[B](f: A => B): MyOption[B] =
单子(MyOption).map () (f)
}
情况下对象My没有一个扩展MyOption[none]
情况下类My一些[A](x: A)扩展MyOption[A]
我们从实现a开始 单孢体
类,它将成为我们所有monad实现的基础. 拥有这个类非常方便,因为只需实现它的两个方法-纯
和 flatMap
-对于特定的单子,您将免费获得许多方法(我们将它们限制为简单的 map
方法,但通常还有许多其他有用的方法,如 序列
和 遍历
的数组 单孢体
s).
我们可以表示 map
的组成 纯
和 flatMap
. 你可以从 flatMap
$flatMap: (T \to M[U]) \to (M[T] \to M[U])$表明它非常接近$map: (T \to U) \to (M[T] \to M[U])$. 不同之处在于中间额外的$M$,但我们可以使用 纯
函数将$U$转换为$M[U]$. 我们用这种方式表达 map
就…而言 flatMap
和 纯
.
这在Scala中工作得很好,因为它有一个高级的类型系统. 它也适用于JS、Python和Ruby,因为它们是动态类型的. 不幸的是, 这对斯威夫特不起作用, 因为它是静态类型的,没有高级类型特性,比如 higher-kinded类型,所以对于斯威夫特,我们必须实现 map
对于每个单子.
还要注意,Option单子已经是一个 事实上的 斯威夫特和Scala等语言的标准, 所以我们对单子的实现使用稍微不同的名字.
现在我们有了一个底 单孢体
类,让我们来看看Option单子的实现. 如前所述,基本思想是Option要么保存一些值(称为 一些
)或者根本没有任何价值(没有一个
).
的 纯
方法只是将值提升为 一些
,而 flatMap
方法检查控件的当前值 Option
-如果是 没有一个
然后返回 没有一个
,如果是 一些
对于基础值,它提取基础值,应用 f()
并返回一个结果.
注意,仅使用这两个函数和 map
因此,不可能出现空指针异常——永远不会. (这个问题 可以 在我们实施的过程中可能会出现 flatMap
方法,但那只是我们代码中检查一次的几行. 在那之后, 我们只需在代码的数千个地方使用Option monad实现,根本不用担心空指针异常.)
让我们深入研究第二个单子:要么. 这与Option单子基本相同,但使用 一些
被称为 正确的
和 没有一个
被称为 左
. 但这一次, 左
也允许有一个潜在的值.
我们需要它,因为它非常方便地表达抛出异常. 如果发生异常,则 要么
将 左(异常)
. 的 flatMap
如果值为,则函数不会进度 左
, 它重复抛出异常的语义:如果发生了异常, 我们停止进一步的执行.
导入单孢体./单子”;
导出类继承单孢体 {
// 纯 :: a -> 要么 a
纯 = (值) => {
返回新 (价值)
}
// flatMap :: # 要么 a -> (a -> 要么 b) -> 要么 b
flatMap = f =>
这.is左 () ?
这样的:
f(这.值)
is左 = () => 这.构造函数.name === '左'
}
导出类左扩展要么 {
构造函数(值){
超级();
这.Value = Value;
}
to字符串 () {
返回“左($ {.值})”
}
}
导出类正确的扩展了要么 {
构造函数(值){
超级();
这.Value = Value;
}
to字符串 () {
返回的权利($ {.值})”
}
}
// attempt :: (() -> a) -> M a
要么.attempt = f => {
尝试{
返回 新 正确的(f())
} catch(e) {
返回 新 左(e)
}
}
要么.纯 = (新 左(null)).纯
从monad导入monad
类(单轴):
# 纯 :: a -> 要么 a
@staticmethod
def纯(价值):
返回正确的(值)
# flat_map :: # 要么 a -> (a -> 要么 b) -> 要么 b
Def flat_map (自我, f):
如果自我.is_left:
回归自我
其他:
返回f(自我.值)
类左():
Def __init__(自我, 值):
自我.Value = Value
自我.is_left = True
类正确():
Def __init__(自我, 值):
自我.Value = Value
自我.is_left = False
require_relative”./单轴的
class 要么 < 单孢体
Attr_accessor:is_left, value
# 纯 :: a -> 要么 a
def自我.纯(值)
正确的.新(值)
结束
# 纯 :: a -> 要么 a
def纯(值)
自我.class.纯(值)
结束
# flat_map :: # 要么 a -> (a -> 要么 b) -> 要么 b
def flat_map (f)
如果is_left
自我
其他的
f.调用(值)
结束
结束
结束
class 左 < 要么
def初始化(值)
@Value = Value
@is_left = true
结束
结束
class 正确的 < 要么
def初始化(值)
@Value = Value
@is_left = false
结束
结束
进口的基础
enum 要么 {
例左(一个)
情况下正确的(B)
static func 纯(_ value: C) -> 要么 {
返回 要么.(价值)
}
func flatMap(_ f: (B) -> 要么) -> 要么 {
切换自{
情况下 .左(让x):
返回 要么.左(x)
情况下 .正确的(让x):
返回f (x)
}
}
func map(f: (B) -> C) -> 要么 {
回归自我.flatMap { 要么.纯(f(0)美元)}
}
}
包的单子
密封特性My要么[+E, +A] {
def flatMap[EE >: E, B](f: A => My要么[EE, B]): My要么[EE, B] =
单子(My要么 (EE, ?]].flatMap(这)(f)
def map[EE >: E, B](f: A => B): My要么[EE, B] =
单子(My要么 (EE, ?]].map () (f)
}
情况下类My左[E](E: E)扩展了My要么[E, Nothing]
情况下类My正确的[A](A: A)扩展了My要么[Nothing, A]
// ...
隐式def my要么单孢体[E] = 新 单孢体[My要么]; ?]] {
def纯 [A](A: A) = My正确的(A)
def flatMap[A, B](ma: My要么[E, A])(f: A => My要么[E, B]): My要么[E, B] = ma match {
情况下 My左(a) => My左(a)
情况下 My正确的(b) => f(b)
}
}
还要注意,捕获异常很容易:您所要做的就是映射 左
to 正确的
. (不过,为了简洁,我们在示例中没有这样做.)
让我们来看看我们需要的最后一个单子:未来单子. 未来 monad基本上是一个值的容器,该值要么现在可用,要么将在不久的将来可用. 你可以用 map
和 flatMap
它将等待未来值被解析,然后再执行依赖于首先被解析的值的下一段代码. 这与JS中的承诺s概念非常相似.
我们现在的设计目标是将不同语言的现有异步api连接到一个一致的基础上. 事实证明,最简单的设计方法是在构造器中使用回调.
而回调设计在JavaScript和其他语言中引入了回调地狱问题, 这对我们来说不是问题, 因为我们使用单子. 事实上, 承诺
对象——的基础 JavaScript对回调地狱的解决方案-是单子本身!
那么未来单子的构造函数呢? 上面有这样的签名:
构造函数 :: ((要么 err a -> void) -> void) -> 未来 (要么 err a)
我们把它分成几部分. 首先,让我们定义一下:
type 回调 = 要么 err a -> void
So 回调
是一个函数,要么是一个错误或一个解析值作为参数,并返回什么. 现在我们的签名是这样的:
构造函数 :: (回调 -> void) -> 未来 (要么 err a)
因此,我们需要为它提供一个函数,该函数不返回任何内容,并在异步计算解析为错误或某个值时立即触发回调. 看起来很容易为任何语言搭建桥梁.
至于未来单子本身的设计,让我们看一下它的内部结构. 关键思想是有一个缓存变量,在未来单子被解析时保存一个值, 或者别的什么都没有. 你可以通过一个回调来订阅未来,如果value被解析,它将立即被触发, 如果不是, 会将回调放到订阅者列表中吗.
一旦未来解决了, 此列表中的每个回调将在单独的线程中使用解析值(或作为事件循环中执行的下一个函数)精确触发一次, 在JS的情况下.注意,小心地使用同步原语是至关重要的, 否则可能出现竞态条件.
基本流程是:启动作为构造函数参数提供的异步计算, 并将它的回调指向我们的内部回调方法. 同时,您可以订阅未来单子并将回调放到队列中. 计算完成后,内部回调方法调用队列中的所有回调. 如果你熟悉响应式扩展(RxJS, Rx斯威夫特等).),它们使用与异步处理非常相似的方法.
未来单子的公共API包括 纯
, map
, flatMap
,就像前面的单子一样. 我们还需要一些方便的方法:
异步
,它接受一个同步阻塞函数,并在一个单独的线程上执行它遍历
,它接受一个值数组和一个将值映射到a的函数 未来
,并返回 未来
已解析值数组的让我们看看结果如何:
导入单孢体./单子”;
import {要么, 左, 正确的} from './ ';
导入{none, 一些}./选项”;
导出类未来扩展单孢体 {
// 构造函数 :: ((要么 err a -> void) -> void) -> 未来 (要么 err a)
构造函数(f) {
超级();
这.订阅用户= [];
这.Cache = none;
f(这.回调)
}
// callback :: 要么 err a -> void
callback = (值) => {
这.缓存= 新 一些(值)
而(这.用户.长度){
Const订阅者=此.用户.转变();
订户(值)
}
}
// subscribe :: (要么 err a -> void) -> void
subscribe = (订阅者) =>
(这.Cache ===无 ? 这.用户.Push(订阅者):订阅者(此).缓存.值))
to承诺 = () => 新 承诺(
(resolve, reject) =>
这.订阅(val => val.is左 () ? 拒绝(val.值):解析(val.值))
)
// 纯 :: a -> 未来 a
纯净=未来.纯
// flatMap :: (a -> 未来 b) -> 未来 b
flatMap = f =>
新未来(
cb => 这.订阅(value => value.is左 () ? Cb (值): f(值).值).订阅(cb))
)
}
未来.异步 = (nodeFunction, ...args) => {
返回 新未来(cb =>
nodeFunction (...args, (err, data) => err ? cb(新 左(err)): cb(新 正确的(data)))
);
}
未来.纯 = value => 新未来(cb => cb(要么.纯(值)))
// 遍历 :: [a] -> (a -> 未来 b) -> 未来 [b]
未来.遍历 = 列表 => f =>
列表.减少(
(acc, elem) => acc.flatMap(values => f(elem).地图(value => [...值,值])),
未来.纯([])
)
从monad导入monad
从选项导入nil,一些
from either import either, 左, 正确的
从functools导入reduce
进口线程
类未来(单轴):
# __init__ :: ((要么 err a -> void) -> void) -> 未来 (要么 err a)
Def __init__(自我, f):
自我.订阅者= []
自我.缓存= nil
自我.信号量=线程.Bounded信号量 (1)
f(自我.回调)
# 纯 :: a -> 未来 a
@staticmethod
def纯(价值):
返回未来(lambda cb: cb.纯(值)))
defexec (f, cb):
试一试:
数据= f()
cb(右)(数据)
Exception as err:
cb(左(err))
Def exec_on_thread(f, cb):
穿线.线程(=未来的目标.执行,参数=[f, cb])
t.开始 ()
def异步(f):
返回未来(lambda cb:未来.exec_on_thread (f, cb))
# flat_map :: (a -> 未来 b) -> 未来 b
Def flat_map (自我, f):
返回未来(
Lambda cb: 自我.订阅(
Lambda值:cb(值) 如果(值).f(value . left.值).订阅(cb)
)
)
# 遍历 :: [a] -> (a -> 未来 b) -> 未来 [b]
def导线(加勒比海盗):
返回lambda f:
Lambda acc, elem: acc.flat_map (
Lambda值:f(elem).地图(
Lambda值:values + [value]
)
), 加勒比海盗,未来.纯([]))
# callback :: 要么 err a -> void
Def callback(自我, 值):
自我.信号量.获得()
自我.缓存= 一些(值)
而(len(自我.用户) > 0):
Sub = 自我.用户.流行(0)
穿线.线程(目标=子,arg游戏=(值))
t.开始 ()
自我.信号量.释放 ()
# subscribe :: (要么 err a -> void) -> void
Def 订阅(自我, 订阅者):
自我.信号量.获得()
如果(自我.缓存.定义):
自我.信号量.释放 ()
订户(自我.缓存.值)
其他:
自我.用户.追加(用户)
自我.信号量.释放 ()
require_relative”./单轴的
require_relative”./不是的
require_relative”./选项”
class 未来 < 单孢体
Attr_accessor:订阅者,:缓存,:信号量
# initialize :: ((要么 err a -> void) -> void) -> 未来 (要么 err a)
def初始化(f)
@订阅者= []
@缓存= 没有美元
@信号量 =队列.新
@信号量.推动(零)
f.调用(方法(回调))
结束
# 纯 :: a -> 未来 a
def自我.纯(值)
未来.新(-> (cb) { cb.电话(.纯(值))})
结束
def自我.异步(f * args)
未来.新(-> (cb) {
线程.新{
开始
cb.电话(右.新(f.电话(* args)))
rescue => e
cb.电话(左.新(e))
结束
}
})
结束
# 纯 :: a -> 未来 a
def纯(值)
自我.class.纯(值)
结束
# flat_map :: (a -> 未来 b) -> 未来 b
def flat_map (f)
未来.新(-> (cb) {
订阅(-> (值) {
如果(值.is_left)
cb.调用(值)
其他的
f.调用(值.值).订阅(cb)
结束
})
})
结束
# 遍历 :: [a] -> (a -> 未来 b) -> 未来 [b]
def自我.遍历(加勒比海盗)
加勒比海盗.减少(未来.Pure ([])) do |acc, elem|
acc.flat_map (-> (values) {
f.调用(elem).地图(-> (值) { values + [value] })
})
结束
结束
# callback :: 要么 err a -> void
def回调(值)
信号量.流行
自我.缓存= 一些.新(值)
而(用户.count > 0)
Sub = 自我.用户.转变
线程.新{
sub.调用(值)
}
结束
信号量.推动(零)
结束
# subscribe :: (要么 err a -> void) -> void
def订阅(用户)
信号量.流行
如果(自我.缓存.定义)
信号量.推动(零)
订阅者.调用(缓存.值)
其他的
自我.用户.推动(用户)
信号量.推动(零)
结束
结束
结束
进口的基础
let background = DispatchQueue(label: "background", attributes: .并发)
class 未来 {
typealias 回调 = (要么) -> Void
var 用户: Array<回调> = Array<回调>()
var 缓存: Maybe<要么> = .没有一个
var 信号量 = Dispatch信号量(值为1)
lazy var callback: callback = {value in
自我.信号量.wait ()
自我.缓存= .一些(值)
当(自我.用户.count > 0) {
让订阅者=自己.用户.流行Last ()
background.异步{
订阅者?(值)
}
}
自我.信号量.信号()
}
init(_ f: @escaping (@escaping 回调) -> Void) {
f(自我.回调)
}
函数c订阅(_ cb: @逃避回调){
自我.信号量.wait ()
切换缓存{
情况下 .没有:
用户.追加(cb)
自我.信号量.信号()
情况下 .一些(让价值):
自我.信号量.信号()
cb(值)
}
}
static func 纯(_ value: B) -> 未来 {
回到未来 { $0(要么.纯(值))}
}
func flatMap(_ f: @escaping (A) -> 未来) -> 未来 {
回到未来 { [weak 自我] cb in
Guard let 这 = 自我 其他的 {返回}
这.中订阅{值
开关值{
情况下 .左(让犯错):
cb(要么.左(err))
情况下 .正确的(让x):
f(x).订阅(cb)
}
}
}
}
func map(_ f: @escaping (A) -> B) -> 未来 {
回归自我.flatMap { 未来.纯(f(0)美元)}
}
static func 遍历(_ 列表: Array, _ f: @escaping (A) -> 未来) -> 未来> {
返回列表.减少(未来>.纯(Array())) { (acc: 未来>, elem: A) in
返回acc.flatMap{元素在
返回f (elem).映射{val in
返回元素+ [val]
}
}
}
}
}
包的单子
导入java.跑龙套.并发.信号量
类My未来[A] {
private var 用户: List[My要么[Exception, A] => Unit] = List()
private var 缓存: MyOption[My要么[Exception, A]] = My没有一个
private val 信号量 = 新 信号量 (1)
def 这(f: (My要么[Exception, A] => Unit) => Unit) {
这()
f(这.回调_)
}
def flatMap[B](f: A => My未来[B]): My未来[B] =
单子(My未来).flatMap(这)(f)
def map[B](f: A => B): My未来[B] =
单子(My未来).map () (f)
def callback(value: myeeither [Exception, A]): Unit = {
信号量.收购
缓存= My一些(值)
用户.foreach { sub =>
= 新 线程 ()
新 Runnable {
def run: Unit = {
子(值)
}
}
)
t.开始
}
订阅者 = List()
信号量.释放
}
def 订阅(sub: My要么[Exception, A] => Unit): Unit = {
信号量.收购
缓存匹配{
情况下 My没有一个 =>
订阅者 = sub::订阅用户
信号量.释放
情况下 My一些(值) =>
信号量.释放
子(值)
}
}
}
对象My未来 {
def 异步[B, C](f: B => C, arg: B): My未来[C] =
新 My未来[C]({ cb =>
= 新 线程 ()
新 Runnable {
def run: Unit = {
尝试{
cb(重现(f (arg)))
} catch {
情况下 e: Exception => cb(My左(e))
}
}
}
)
t.开始
})
def 遍历[A, B](列表: List[A])(f: A => My未来[B]): My未来[List[B]] = {
列表.fold正确的(单细胞生物(My未来).纯(List[B]())) { (elem, acc) =>
单子(My未来).flatMap(acc) ({ values =>
单子(My未来).地图(f(elem)) { value => value :: values }
})
}
}
}
// ...
隐式val my未来单孢体 =新的单子(My未来) {
def纯 [A](A: A): My未来[A] =
新 My未来[A]({ cb => cb(my要么单孢体[Exception].纯(a))})
def flatMap[A, B](ma: My未来[A])(f: A => My未来[B]): My未来[B] =
新 My未来[B]({ cb =>
ma.订阅(_ match {)
情况下 My左(e) => cb(My左(e))
情况下 My正确的(a) => f(a).订阅(cb)
})
})
}
的公共API是如何 未来
不包含任何底层细节,比如线程,信号量,或者其他类似的东西. 你所需要的就是用回叫提供一些东西,就是这样!
好的,让我们试着用单子来做一个实际的程序. 假设我们有一个包含url列表的文件,我们希望并行地获取每个url. 然后,为了简洁起见,我们希望将每个响应缩减为200字节,并打印出结果.
我们首先将现有的语言api转换为一元接口(参见函数) readFile
和 获取
). 现在我们有了这个,我们可以把它们组合起来得到最终的结果. 请注意,链本身是超级安全的,因为所有的细节都包含在单子中.
import {未来} from './未来”;
import {要么, 左, 正确的} from './ ';
从“文件”中导入{readFile};
从' HTTPS '导入HTTPS;
const getResponse = url =>
新未来(cb => http.get(url, res => {
Var body = ";
res.on('data', data => body += data);
res.on('结束', data => cb(新 正确的(body)));
res.on('error', err => cb(新 左(err)))
}))
const getShortResponse = url => getResponse(url).地图(resp => resp.substring (0, 200))
未来
.异步(readFile,资源/ url.txt”)
.地图(data => data.to字符串 ().分割(“\ n”))
.flatMap(urls => 未来.遍历(url) (getShortResponse))
.地图(控制台.日志)
进口http.客户端
进口线程
导入的时间
进口操作系统
从未来进口
from either import either, 左, 正确的
Conn = HTTP.客户端.HTTPSConnection(“在.维基百科.org”)
def read_file_sync (uri):
Base_dir =操作系统.路径.dirname(__file__) #<-- absolute dir the script is in
路径= OS.路径.加入(base_dir uri)
其中open(路径)为f:
返回f.read ()
def 获取_sync (uri):
康涅狄格州.请求(“得到”,uri)
R = 康涅狄格州.getresponse ()
返回r.read ().解码(“utf - 8”)(200):
def read_file (uri):
回到未来.异步(λ:read_file_sync (uri))
def获取(uri):
回到未来.异步(λ:获取_sync (uri))
def主要(args = 没有一个):
Lines = read_file("../资源/ url.txt”).映射(lambda res: res.split行s ())
内容=行.flat_map (lambda url:未来.遍历(url)(获取)
输出=内容.Map (lambda res: print("\n").加入(res)))
如果__name__ == "__main__":
main ()
需要的./lib/future”
需要“net/http”
需要“uri”
信号量=队列.新
def读(uri)
未来.异步(-> () { File.读(uri)})
结束
def获取(url)
未来.异步(-> () {
uri = uri (url)
Net:: HTTP.get_response (uri).身体[0..200]
})
结束
读(“资源/ url.txt”)
.地图(-> (x) { x.分割(“\ n”)})
.flat_map (-> (urls) {
未来.遍历(urls, -> (url) { 获取(url) })
})
.地图(-> (res) { puts res; 信号量.推动(true)})
信号量.流行
进口的基础
enum Err:错误{
一些(字符串)
}
func readFile(_ 路径: 字符串) -> 未来 {
回到未来 { callback in
background.异步{
让url = url (fileURLWithPath:路径)
Let text = try? 字符串(contentsOf: url)
如果让res = text {
callback(要么.纯(res))
} 其他的 {
callback(要么.左(犯错.一些(“读取url错误.txt”)))
}
}
}
}
func 获取Url(_ url: 字符串) -> 未来 {
回到未来 { callback in
background.异步{
让url = url(字符串:url)
let 任务 = URLSession.共享.dataTask (: url!){(数据,响应,错误)in
如果让err = error {
callback(要么.左(err))
返回
}
guard让nonEmptyData = data 其他的 {
callback(要么.左(犯错.一些(“空响应”)))
返回
}
guard let result = 字符串(data: nonEmptyData, encoding: 字符串).编码.Utf8) 其他的 {
callback(要么.左(犯错.一些("无法解码响应"))
返回
}
让index = result.指数(结果.开始Index, offsetBy: 200)
callback(要么.纯(字符串(结果.. [字符串] in
data.组件(separatedBy:“\ n”).filter {(行: 字符串) in !行.isEmpty}
}.flatMap {url在
回到未来.遍历(url) {url在
返回获取Url (url)
}
}.映射{响应
打印(反应)
}
RunLoop.main.run ()
导入scala.io.Source
导入java.跑龙套.并发.信号量
进口单子._
扩展App {
val 信号量 = 新 信号量 (0)
def readFile(name: 字符串): My未来[List[字符串]] =
My未来.异步[字符串, List[字符串]](filename => Source.fromResource(文件名).get行.toList的名字)
def 获取(url: 字符串): My未来[字符串] =
My未来.异步(字符串,字符串)(
uri => Source.fromURL (uri).mk字符串.substring (0, 200),
url
)
Val future = for {
urls <- readFile("urls.txt”)
entries <- My未来.遍历(url) (_)
} yield {
println(条目)
信号量.释放
}
信号量.收购
}
这就是实践中的it-monad解决方案. 您可以找到一个包含本文所有代码的repo GitHub上.
对于这个简单的基于单子的程序, 使用我们之前编写的所有代码可能看起来有点多余. 但这只是初始设置,它的大小将保持不变. 从现在开始想象一下, 使用了单体, 你可以写很多异步代码, 不用担心线程, 竞态条件, 信号量, 异常, 或者空指针! 太棒了!
monad是一个抽象接口,它定义了“纯”和“flatMap”函数. Pure允许您将普通值转换为一元值. FlatMap允许将普通参数函数应用于一元参数.
单子在函数式编程中被广泛使用. 他们的主要目标是在一个地方隔离任何不可靠和危险的副作用, 因此,您可以在应用程序的所有其他部分享受安全编程.
从技术上讲,单子是一个内函子,意思是它把一个类别映射到它自己. 但是你可以把那个函子的像看作一个具有一元副作用的新范畴.
单元编程是一种将不同的单元值组合成一个大单元的技术. 之后就很容易处理所有的副作用了, 因为它们集中在一个单子上, 而不是很多单子.
莫斯科,俄罗斯
2017年6月26日成为会员
Alexey (MEcon)精通几种语言,更喜欢函数式编程, 尤其是Scala, 为了减少浪费在捉虫子上的时间.
世界级的文章,每周发一次.
世界级的文章,每周发一次.