所有monad文章经常声明,monad允许您按顺序排列效果。
但是简单的组成呢?不会
f x = x + 1
g x = x * 2
result = f g x
需要在
g x
之前计算f ...
?monad可以做到一样但可以处理效果吗?
最佳答案
免责声明:Monad有很多东西。众所周知,它们很难解释,因此,在此我将不尝试解释一般的monad,因为这个问题并不需要。我将假定您对Monad
接口及其对某些有用的数据类型(如Maybe
,Either
和IO
)的工作方式有基本的了解。
有什么作用?
您的问题以一个音符开头:
所有monad文章经常声明,monad允许您按顺序排列效果。
嗯这是有趣的。实际上,有几个原因很有趣,您已经确定了其中一个原因:这意味着单子可以让您创建某种排序。没错,但这只是图片的一部分:它还指出排序发生在效果上。
不过,这就是……什么是“效果”?将两个数字相加会产生效果吗?根据大多数定义,答案是否定的。打印一些东西到stdout会怎么样?在这种情况下,我想大多数人都会同意答案是肯定的。但是,请考虑一些更微妙的问题:通过产生Nothing
效应来使计算短路吗?
错误影响
让我们来看一个例子。考虑以下代码:
> do x <- Just 1
y <- Nothing
return (x + y)
Nothing
该示例的第二行由于
Monad
的Maybe
实例而导致“短路”。可以认为是效果吗?从某种意义上说,我认为是这样,因为它不是本地的,但从另一种意义上来说,可能不是。毕竟,如果交换x <- Just 1
或y <- Nothing
行,结果仍然相同,因此顺序无关紧要。但是,请考虑使用
Either
而不是Maybe
的更为复杂的示例:> do x <- Left "x failed"
y <- Left "y failed"
return (x + y)
Left "x failed"
现在,这更有趣。如果现在交换前两行,则会得到不同的结果!仍然,这是否像您在问题中提到的那样代表“效应”?毕竟,这只是一堆函数调用。如您所知,
do
表示法只是用于>>=
运算符的一类替代语法,因此我们可以将其扩展:> Left "x failed" >>= \x ->
Left "y failed" >>= \y ->
return (x + y)
Left "x failed"
我们甚至可以将
>>=
运算符替换为Either
特定的定义,以完全摆脱单子:> case Left "x failed" of
Right x -> case Left "y failed" of
Right y -> Right (x + y)
Left e -> Left e
Left e -> Left e
Left "x failed"
因此,很明显单子确实会施加某种排序,但这并不是因为它们是单子并且单子是魔术,而是因为它们恰好启用了一种看起来比Haskell通常允许的不纯净的编程风格。
单子和状态
但这也许令您不满意。错误处理并不引人注目,因为它只是短路,实际上没有任何排序结果!好吧,如果我们找到一些稍微复杂一些的类型,我们可以做到。例如,考虑
Writer
类型,它允许使用monadic接口进行某种“记录”:> execWriter $ do
tell "hello"
tell " "
tell "world"
"hello world"
这比以前更加有趣,因为现在
do
块中的每次计算结果都没有使用,但是仍然会影响输出!这显然是有副作用的,秩序显然非常重要!如果我们重新排序tell
表达式,我们将得到非常不同的结果:> execWriter $ do
tell " "
tell "world"
tell "hello"
" worldhello"
但这怎么可能呢?好了,同样,我们可以重写它以避免
do
表示法:execWriter (
tell "hello" >>= \_ ->
tell " " >>= \_ ->
tell "world")
我们可以再次为
>>=
内联Writer
的定义,但这太长了,无法在此处进行说明。但是,要点是,Writer
只是一个完全普通的Haskell数据类型,它不执行任何I / O或类似操作,但是我们已经使用monadic接口创建了看起来像有序效果的东西。通过使用
State
类型创建看起来像可变状态的接口,我们可以走得更远:> flip execState 0 $ do
modify (+ 3)
modify (* 2)
6
再一次,如果我们对表达式重新排序,则会得到不同的结果:
> flip execState 0 $ do
modify (* 2)
modify (+ 3)
3
显然,monad是一种有用的工具,可用于创建看起来有状态且具有明确定义的顺序的接口,尽管实际上只是普通的函数调用。
为什么单子可以这样做?
是什么赋予单子这种力量?嗯,它们不是魔术,而是普通的纯Haskell代码。但是请考虑
>>=
的类型签名:(>>=) :: Monad m => m a -> (a -> m b) -> m b
注意第二个参数如何依赖于
a
,而获得a
的唯一方法是从第一个参数开始?这意味着>>=
需要先“运行”第一个参数以产生值,然后才能应用第二个参数。这与评估顺序无关,而与实际编写将进行类型检查的代码有关。现在,Haskell确实是一种惰性语言。但是Haskell的懒惰对此并不重要,因为所有这些代码实际上都是纯代码,即使使用
State
的示例也是如此!它只是一种模式,以一种纯粹的方式对看起来有状态的计算进行编码,但是如果您自己真正实现了State
,您会发现它只是在>>=
函数的定义中传递了“当前状态” 。没有任何实际的突变。就是这样。 Monad凭借其接口对如何评估其参数施加了排序,并且
Monad
实例利用该接口来构成有状态的接口。但是,您不需要像Monad
那样进行评估排序。显然,在(1 + 2) * 3
中加法将在乘法之前求值。但是
IO
呢?好吧,你懂了。问题出在这里:
IO
是魔术。Monad不是魔术,但
IO
是魔术。以上所有示例都是纯函数式的,但是显然读取文件或写入stdout并非纯函数。那么IO
是如何工作的呢?好吧,
IO
是由GHC运行时实现的,您不能自己编写它。但是,为了使其能够与Haskell的其余部分很好地配合使用,需要有一个明确定义的评估顺序!否则,事情将以错误的顺序打印出来,其他各种地狱也会崩溃。嗯,事实证明
Monad
的界面是确保评估顺序可预测的好方法,因为它已经适用于纯代码。因此,IO
利用相同的接口来确保评估顺序相同,并且运行时实际上定义了评估的含义。但是,不要被误导!您不需要monad即可使用纯语言进行I / O,也不需要
IO
具有monadic效果。 Early versions of Haskell experimented with a non-monadic way to do I/O,以及此答案的其他部分说明如何获得纯正的单子波效果。请记住,单子不是特殊的或神圣的,它们只是Haskell程序员发现的一种模式,因为它具有多种属性。
关于haskell - 单声道,组成和计算顺序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41310361/