Haskell简介,98版
后面 下一个 顶部


9  关于Monad

Haskell的许多新人对以下概念感到困惑:单子.Haskell中经常遇到单体:IO系统已构建使用monad,提供了monad的特殊语法(表达式),标准库包含一个完整的专用模块到单子。在本节中,我们将更详细地探讨单数编程。

这部分可能不如其他部分“温和”。在这里,我们不仅要解决涉及单子的语言特征,还要解决试着揭示更大的图景:为什么单子如此重要工具及其使用方法。没有解释适用于每个人的单子的单一方式;更多有关解释,请访问haskell.org网站.另一个好的Wadler介绍了使用monad进行实际编程函数式编程单子函数[10].

9.1  Monadic课程

前奏曲包含许多定义单子的类在Haskell中使用。这些类基于monad构造范畴理论;而范畴理论术语提供了一元类和操作的名称,而不是有必要钻研抽象数学以获得直觉了解如何使用monadic类。

monad是在多态类型(例如IO(输入输出). Themonad本身由实例声明定义将类型与部分或全部一元类,Functor(仿真器),莫纳德,MonadPlus系列。没有一个单元类是可派生的。此外IO(输入输出)前奏曲中的另外两种类型是单子的成员类:列表([])和也许 吧.

数学上,单子由一组法律那应该够了用于单子运算。这种法律观念并非只有monads:Haskell包括其他操作受法律管辖,至少是非正式的。例如,x/=y不是(x==y)对于任何类型的值都应该是相同的相比。然而,这并不能保证:==/=中的单独方法方程式我们无法保证===/以这种方式关联。在同样的意义上,这里提出的单子法不是由Haskell,但一元类的任何实例都应该遵守。单子定律揭示了单子的基本结构:通过研究这些定律,我们希望了解单子是如何形成的已使用。

这个Functor(仿真器)课程,已经在第节中讨论过5,定义了单一操作:功能地图。map函数将操作应用于容器中的对象(多态类型可以看作容器用于其他类型的值),返回形状相同。这些法律适用于功能性维修计划在课堂上Functor(仿真器):

fmap id=身份证
fmap(f.g)=fmap图。fmap克

这些法律确保容器形状保持不变功能性维修计划容器内的物品不会被重新安排映射操作。

这个莫纳德类定义了两个基本运算符:>>=(绑定)和返回.

中缀1>>,>>=
Monad m类,其中
(>>=)::m a->(a->m b)->m b
(>>)::ma->m b->m b
返回::a->ma
失败::String->m a

m>>k=m>>=\_->k

绑定操作,>>>>=,组合两个单值,同时这个返回操作将值注入monad(容器)。签字人:>>=帮助我们需要了解此操作:ma>>=\v->mb组合了一元值妈妈包含值类型为和一个运行的函数在值上v(v)类型为,返回一元值毫巴. The结果是合并妈妈毫巴一元值包含b条. The>>当函数不需要第一个一元运算符。

当然,绑定的确切含义取决于单子。对于例如在IO monad中,x>>=y依次执行两个操作,将第一个结果传递给第二个。对于另一个内置单子、列表和也许 吧类型,这些单子操作可以通过从一个值传递零个或多个值来理解计算到下一个。我们稍后将看到这方面的示例。

这个语法为单数链提供了一个简单的缩写操作。基本翻译在中捕获遵循两条规则:

执行e1;e2=e1>>e2
做p<-e1;e2=e1>>=\p->e2

当第二种形式的模式是可以反驳的,模式匹配失败调用失败操作。这可能会引发错误(如在中IO(输入输出)monad)或返回“零”(如列表monad中所示)。因此更复杂的翻译是

做p<-e1;e2=e1>>=(p->e2的情况v;_->失败“s”)

哪里“s”是一个字符串,用于标识陈述用于错误消息中。例如,在I/O monad中,动作,如“a”<-getChar将呼叫失败如果字符类型不是“a”。这反过来会终止程序,因为在I/O单体失败电话错误.

管辖法律>>=返回是:

返回a>>=k=卡拉
m>>=返回=
xs>>=返回。(f)=格式映射f xs
m>>=(\x->k x>>=h)=(m>>=k)>>=h

班级MonadPlus系列用于具有要素和一个操作:

class(Monad m)=>MonadPlus m,其中
mzero::毫安
mplus::ma->ma->ma

零元素遵循以下定律:

m>>=\x->mzero=mzero(零点)
mzero>>=米=mzero(零点)

对于列表,零值为[],空列表。I/O monad具有没有零元素,并且不是此类的成员。

管理mplus(mplus)操作员如下:

m`mplus`mzero=
mzero`mplus`m=

这个mplus(mplus)运算符是列表monad中的普通列表串联。

9.2  内置Monads

考虑到单子操作和管理它们的法律我们建造?我们已经详细检查了I/O monad,因此我们从另外两个内置单子开始。

对于列表,一元绑定涉及将一组列表中每个值的计算。与列表一起使用时签名>>=变为:

(>>=)::[a]->(a->[b])->[b]

也就是说,给定一个列表的和映射在上列表,共个b条的,绑定将此函数应用于在中输入并返回所有生成的b条的连接到列表。这个返回函数创建一个单例列表。这些操作应该已经很熟悉了:列表理解很容易用一元运算表示为列表定义。以下三个对于同一事物,表达式都是不同的语法:

[(x,y)| x<-[1,2,3],y<-[1,1,2,3',x/=y]

do x<-[1,2,3]
y<-[1,2,3]
真<-返回(x/=y)
返回(x,y)

[1,2,3]>>=(\x->[1,2,4]>>=(\y->返回(x/=y)>>=
(\r->True的案例r->return(x,y)
_->失败“”))

此定义取决于失败在这个单子里空列表。基本上,每个<-正在生成一组值其被传递到一元计算的剩余部分中。因此x<-[1,2,3]调用一元计算的其余部分三次,每个列表元素一次。返回的表达,(x,y),将是评估围绕它的所有可能的绑定组合。从这个意义上说,单子列表可以被认为是描述多值参数的函数。例如,此函数:

mvLift2::(a->b->c)->[a]->[b]->[c]
mvLift2 f x y=做x’<-x
y’<-y
返回(f x“y”)

转动两个参数的普通函数((f))转换为函数多个值(参数列表),为每个可能的值返回一个值两个输入参数的组合。例如,

mvLift2(+)[1,3][10,20,30] => [11,21,31,13,23,33]
mvLift2(\a b->[a,b])“ab”“cd” => [“ac”、“ad”、“bc”和“bd”]
mvLift2(*)[1,2,4][] => []

此函数是提升M2中的函数monad库。你可以把它看作是从在单子列表之外,(f),在其中计算的列表monad中具有多个值。

为定义的monad也许 吧类似于列表monad:值没有什么充当[]只有x作为【x】.

9.3  使用Monad

解释一元运算符及其相关定律并没有真正展示单子的优点。他们真正提供的是模块化也就是说,通过单独定义一个操作,我们可以以允许新功能透明地融入单子中。韦德勒的论文[10]是单子的一个很好的例子用于构建模块化程序。我们将从一个单子开始直接从本文中,状态monad,然后构建一个更具有类似定义的复杂单子。

简单地说,围绕状态类型构建的状态单体S公司这样地:

data SM a=SM(S->(a,S))--一元类型

例如Monad SM,其中
--定义状态传播
SM c1>>=fc2=SM(\s0->让(r,s1)=c1 s0
SM c2=fc2 r英寸
c2 s1)
返回k=SM(\s->(k,s))

--从单子中提取状态
读取SM::SM S
readSM=SM(\s->(s,s))

--更新monad的状态
updateSM::(S->S)->SM()--更改状态
更新SM f=SM(\s->((),f s))

--在SM monad中运行计算
运行SM::S->SM a->(a,S)
运行SM s0(SM c)=c s0

此示例定义了一个新类型,性虐待,是一个计算隐式携带类型S公司。即,类型的计算SM吨定义类型的值同时也与(读写)类型状态交互S公司.定义性虐待很简单:它由接受声明并生成两个结果:返回值(任何类型)和已更新状态。这里不能使用类型同义词:我们需要类型名喜欢性虐待可以在实例声明中使用。这个新类型这里经常使用声明,而不是数据.

这个实例声明定义了monad的“管道”:如何序列二计算和空计算的定义。排序(>>=运算符)定义计算(由建造师性虐待)超过了首字母状态,第0集,到c1级,然后传递由此得出的值计算,第页,返回第二次计算的函数,二氧化碳。最后c1级被传递到二氧化碳总的结果是二氧化碳.

定义返回更容易:返回不会改变状态;它只为monad带来一个值。

While期间>>=返回是基本的一元测序操作,我们还需要一些一元原语.一元原语是简单地说就是一个使用monad抽象内部的操作敲入使monad工作的“轮子和齿轮”。例如,在IO(输入输出)monad,运算符,如输入字符是原始的,因为他们交易内部工作方式IO(输入输出)莫纳德。类似地,我们的状态monad使用两个基本体:读SM更新SM请注意,这些取决于论单子的内部结构&对定义的改变这个性虐待类型需要更改这些原语。

定义读取SM更新SM都很简单:读SM带来观察单子的状态更新SM允许用户更改monad中的状态。(我们也可以使用写入SM作为一种原始但更新通常是一种更自然的方式与国家打交道)。

最后,我们需要一个在monad中运行计算的函数,运行SM。这需要一个初始状态和一个计算,并产生计算的返回值和最终状态。

放眼全局,我们正在尝试定义一个作为一系列步骤的整体计算(函数类型为山猫a),使用排序>>=返回。这些步骤可能会相互影响状态(通过读SM更新SM)或者可以忽略该状态。然而,状态的使用(或不使用)是隐藏的:我们不调用或者根据是否对我们的计算进行不同的排序他们使用S公司.

我们没有使用这个简单的状态单体给出任何示例,而是接下来是一个更复杂的示例,其中包括状态monad。我们定义了一个小嵌入式语言资源利用计算。也就是说,我们构建了一种特殊用途的语言,它被实现为一组Haskell类型和功能。这些语言使用Haskell的基本工具,函数和类型,以构建操作库以及专门针对感兴趣的领域定制的类型。

在这个示例中,考虑一个需要某种类型的资源。如果资源可用,则继续计算;资源不可用,计算挂起。我们使用类型R(右)使用我们的monad控制的资源来表示计算。定义R(右)如下所示:

数据R a=R(资源->(资源,任一a(R a)))

每个计算都是从可用资源到剩余资源的函数资源,加上类型为的结果,或a暂停计算,类型注册会计师,捕获完成的工作到了资源耗尽的地步。

这个莫纳德的实例R(右)如下所示:

实例Monad R,其中
R c1>>=fc2=R(\R->案例c1 R
(r',左v)->让r c2=fc2 v in
c2 r’
(r',右pc1)->(r'、右(pc1>>=fc2))
return v=R(\R->(R,(左v))

这个资源类型的使用方式与中的状态相同州monad。该定义如下:将二者结合`足智多谋的计算,c1级fc2型(产生函数二氧化碳),将初始资源传递到c1级。结果将是任何一个

悬架必须考虑第二个计算:pc1型仅挂起第一次计算,c1级,所以我们必须绑定二氧化碳从而暂停整体计算。定义返回移动时保持资源不变v(v)进入单子。

此实例声明定义了monad的基本结构,但不确定如何使用资源。这个单子可能是用于控制多种类型的资源或实现多种不同的资源使用策略的类型。我们将演示一个非常简单的以资源定义为例:我们选择资源成为一名整数,表示可用的计算步骤:

类型资源=整数

除非没有可用的步骤,否则此函数将执行一个步骤:

步骤::a->R a
步骤v=c,其中
c=R(\R->如果R/=0,则(R-1,左v)
else(r,右c)

这个左侧赖特构造函数是要么类型。此函数用于继续计算R(右)通过返回v(v)只要至少有一个计算步骤资源可用。如果没有可用的步骤函数挂起电流计算(此暂停捕获于c(c))并通过了这个暂停的计算返回到monad。

到目前为止,我们已经有了定义“足智多谋”序列的工具计算(monad),我们可以表达一种形式的资源使用使用最后,我们需要解决如何在这个monad被表达。

在我们的monad中考虑一个增量函数:

inc::R整数->R整数
inc i=做i值<-i
步骤(iValue+1)

这将增量定义为计算的单个步骤。这个<-从monad中提取参数值所必需的;的类型iValue公司整数而不是R整数.

然而,与增量函数的标准定义。我们可以改为“穿衣服”吗向上”现有操作,如+所以他们在我们的修道院工作世界?我们将从一套开始提升功能。这些将现有功能引入monad。考虑定义属于提升1(这与提升M1在中找到莫纳德库):

提升1::(a->b)->(R a->R b)
lift1 f=\ra1->执行a1<-ra1
台阶(fa1)

这需要一个单一参数的函数,(f),并创建中的函数R(右)只需一步即可执行提升的功能。使用提升1,股份有限公司成为

inc::R整数->R整数
inc i=提升1(i+1)

这是更好的,但仍然不理想。首先,我们添加提升2:

提升2::(a->b->c)->(R a->R b->R c)
lift2 f=\ra1 ra2->执行a1<-ra1
a2<-ra2
台阶(f a1 a2)

请注意,此函数显式设置了提升函数:计算屈服a1级发生在计算a2类.

使用提升2,我们可以创建一个新版本==在中R(右)单子:

(==*)::序号a=>R a->R a->R Bool
(==*)=升2(==)

我们必须为这个新函数使用稍微不同的名称,因为==已被占用,但已被占用有些情况下,我们可以用相同的名字来称呼被提升者和未被提升者功能。此实例声明允许中的所有运算符号码用于R(右):

实例编号a=>编号(R a),其中
(+)=提升2(+)
(-)=提升2(-)
negate=lift1取反
(*)=提升2(*)
abs=提升1 abs
fromInteger=返回。from整数

这个from整数函数隐式应用于所有整数Haskell程序中的常量(参见第节10.3);此定义允许整数常量具有以下类型R整数.最后,我们可以以一种完全自然的方式编写增量:

inc::R整数->R整数
inc x=x+1

请注意,我们无法提升等式以与号码类:的签名==*与允许的不兼容过载==由于==*R臂而不是布尔.

要在中表达有趣的计算R(右)我们需要一个有条件的。因为我们不能使用如果(要求测试类型布尔而不是R Bool公司),我们将该函数命名为如果R:

ifR::R Bool->R a->R a->R a
ifR tst thnel=do t<-tst
如果t,则其他为els

现在,我们准备在R(右)单子:

事实::R整数->R整数
事实x=ifR(x==*0)1(x*事实(x-1))

这和普通的阶乘函数不太一样,但是仍然很可读。提供新定义的想法现有操作,如+如果是创造的重要组成部分Haskell中的嵌入式语言。单声道特别适用于将这些嵌入式语言的语义封装在一个干净的模块化方式。

我们现在已经准备好实际运行一些程序了。此函数运行中的程序给定最大计算步骤数:

运行::Resource->R a->Maybe a
运行s(R p)=的情况
(_,左v)->仅v
_->没有

我们使用也许 吧类型来处理计算未在分配的步数内完成。我们现在可以计算

运行10(事实2) => 只有2个
运行10(事实20) => 没有什么

最后,我们可以添加一些更有趣的功能莫纳德。考虑以下功能:

(|||)::Ra->Ra->R a

这将并行运行两个计算,返回第一个要完成的。此函数的一个可能定义是:

c1||c2=oneStep c1(\c1'->c2|||c1')
其中
一步::R a->(R a->R a)->R a
单步(R c1)f=
R(\R->第c1种情况,共1种
(r',左v)->(r+r'-1,左v
(r',Right c1')->--r'必须为0
设R next=f c1’in
下一个(r+r'-1)

这需要一步c1级,返回其值c1级完成或,如果c1级返回挂起的计算(c1’),它评估c2||c1'. The一步到位函数在其参数,返回计算值或传递余数的计算(f).定义一步到位很简单:它给予c1级1作为其资源参数。如果最终值为到达时,返回此值,调整返回的步长计数(它是计算可能在不执行任何步骤后返回,因此返回的资源计数不一定是0)。如果计算挂起,一个修补的资源计数被传递到最终继续。

我们现在可以计算如下表达式运行100(事实(-1)|||(事实3))由于两个计算是交错的,因此没有循环。(我们的定义事实循环用于-1). 在这个基础上可以有很多变化结构。例如,我们可以扩展状态以包含跟踪计算步骤。我们还可以将此monad嵌入标准IO(输入输出)monad,允许在外部世界。虽然这个示例可能比本教程中的其他示例更高级,它说明了monad作为定义系统的基本语义。我们还将此示例作为一个模型提供一个小的领域特定语言Haskell就是这样特别擅长定义。许多其他DSL都是在年开发的哈斯克尔;看见haskell.org网站更多示例。特别是感兴趣的是反应动画语言Fran和哈斯科尔计算机音乐语言。


Haskell 98版简介
后面 下一个 顶部