Monads,组成和计算顺序

dmz*_*rsk 3 monads haskell composition

所有monad文章经常陈述,monad允许你按顺序排列效果.

但是简单的构图怎么样?是不是

f x = x + 1
g x = x * 2

result = f g x
Run Code Online (Sandbox Code Playgroud)

需要g x先计算f ...

单子会做同样的事情,但处理效果?

Ale*_*ing 13

免责声明:Monads是很多东西.他们是出了名的难以解释,所以我不会试图解释是什么单子一般在这里,因为这个问题不要求这一点.我会认为你对什么是基本掌握Monad接口,以及它是如何工作的一些有用的数据类型,如Maybe,EitherIO.


有什么影响?

您的问题以便笺开头:

所有monad文章经常陈述,monad允许你按顺序排列效果.

嗯.这是有趣的.实际上,有趣的原因有几个,其中一个原因已经确定:它暗示monads可以让你创建某种排序.这是真的,但这只是问题的一部分:它指出,排序发生在影响.

这是事情,但是......什么是"效果"?将两个数字加在一起效果?在大多数定义下,答案是否定的.打印东西到stdout怎么样,这是一种效果吗?在这种情况下,我认为大多数人会同意答案是肯定的.但是,考虑一些更微妙的东西:通过产生Nothing效果来短路计算?

错误效果

我们来看一个例子吧.请考虑以下代码:

> do x <- Just 1
     y <- Nothing
     return (x + y)
Nothing
Run Code Online (Sandbox Code Playgroud)

由于Monad实例的原因,该示例的第二行"短路" Maybe.这可能被认为是一种影响吗?从某种意义上说,我是这么认为的,因为它是非本地的,但在另一种意义上,可能不是.毕竟,如果换行x <- Just 1y <- Nothing换行,结果仍然相同,所以排序无关紧要.

但是,请考虑使用一个稍微复杂的示例,Either而不是Maybe:

> do x <- Left "x failed"
     y <- Left "y failed"
     return (x + y)
Left "x failed"
Run Code Online (Sandbox Code Playgroud)

现在这更有趣了.如果你现在交换前两行,你会得到不同的结果!不过,这是否就像你在问题中提到的"效果"一样?毕竟,它只是一堆函数调用.如您所知,do符号只是>>=运算符大量使用的替代语法,因此我们可以将其展开:

> Left "x failed" >>= \x ->
    Left "y failed" >>= \y ->
      return (x + y)
Left "x failed"
Run Code Online (Sandbox Code Playgroud)

我们甚至可以>>=Either-specific定义替换运算符以完全摆脱monad:

> 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"
Run Code Online (Sandbox Code Playgroud)

因此,monad确实强加了某种排序,但这并不是因为它们是monad和monad是神奇的,只是因为它们碰巧启用了一种看起来比Haskell通常允许的更不纯的编程风格.

Monads和州

但也许这对你来说并不令人满意.错误处理并不引人注目,因为它只是短路,它实际上并没有对结果进行任何排序!好吧,如果我们找到一些稍微复杂的类型,我们就可以做到.例如,考虑Writer类型,它允许使用monadic接口进行某种"日志记录":

> execWriter $ do
    tell "hello"
    tell " "
    tell "world"
"hello world"
Run Code Online (Sandbox Code Playgroud)

这比以前更有趣,因为现在do块中每次计算的结果都没有使用,但它仍会影响输出!这显然是副作用,顺序显然非常重要!如果我们重新排序tell表达式,我们会得到一个非常不同的结果:

> execWriter $ do
    tell " "
    tell "world"
    tell "hello"
" worldhello"
Run Code Online (Sandbox Code Playgroud)

但这怎么可能呢?那么,我们可以重写它以避免do表示法:

execWriter (
  tell "hello" >>= \_ ->
    tell " " >>= \_ ->
      tell "world")
Run Code Online (Sandbox Code Playgroud)

我们可以>>=再次Writer列出这个定义,但是这里的描述太长了.但问题是,这Writer只是一个完全普通的Haskell数据类型,不执行任何I/O或类似的操作,但我们使用monadic接口创建看起来像有序效果的东西.

我们可以通过使用类型创建一个看起来像可变状态的接口来进一步State:

> flip execState 0 $ do
    modify (+ 3)
    modify (* 2)
6
Run Code Online (Sandbox Code Playgroud)

再一次,如果我们重新排序表达式,我们会得到不同的结果:

> flip execState 0 $ do
    modify (* 2)
    modify (+ 3)
3
Run Code Online (Sandbox Code Playgroud)

很明显,monad是一个有用的工具,用于创建看起来有状态的接口,并且具有明确定义的顺序,尽管实际上只是普通的函数调用.

为什么monad可以做到这一点?

是什么赋予monads这种力量?好吧,它们不是魔术 - 它们只是普通的纯Haskell代码.但请考虑以下类型的签名>>=:

(>>=) :: Monad m => m a -> (a -> m b) -> m b
Run Code Online (Sandbox Code Playgroud)

注意第二个参数是如何依赖的a,获得一个的唯一方法a是从第一个参数?这意味着应用第二个参数之前>>=需要"运行"第一个参数以生成值.这与评估顺序没有关系,因为它与实际编写将要进行类型检查的代码有关.

现在,Haskell确实是一种懒惰的语言.但是Haskell的懒惰并不重要,因为所有这些代码实际上都是纯粹的,即使是使用的例子State!它只是一种模式,它以纯粹的方式编码看似有状态的计算,但如果你实际上State自己实现了,你会发现它只是在>>=函数定义中传递"当前状态" .没有任何实际的突变.

就是这样.Monads凭借它们的接口,对如何评估它们的参数进行了排序,以及Monad利用它来构建有状态接口的实例.你并不需要 Monad有评估排序,不过,因为你发现; 显然在(1 + 2) * 3加法中将在乘法之前进行评估.

但是呢IO

好的,你有我.这是问题:IO是魔法.

Monads不是魔术,但是IO.以上所有示例都是纯函数,但显然读取文件或写入stdout并不纯粹.那么heck怎么IO运作?

好吧,IO由GHC运行时实现,你不能自己编写.但是,为了使其与Haskell的其余部分很好地协同工作,需要有一个明确定义的评估顺序!否则,事情将以错误的顺序打印出来,各种其他的地狱将会破裂.

好吧,事实证明,Monad界面是确保评估顺序可预测的好方法,因为它已经适用于纯代码.因此,IO利用相同的接口来保证评估顺序是相同的,并且运行时实际上定义了评估的含义.

但是,不要被误导!您不需要monad以纯语言进行I/O操作,并且您不需要IO具有monadic效果. 早期版本的Haskell使用非monadic方式进行I/O测试,此答案的其他部分解释了如何获得纯粹的monadic效果.请记住,monad不是特殊的或神圣的,它们只是Haskell程序员因其各种属性而发现有用的模式.


Dan*_*ner 6

是的,您建议的功能对标准数字类型是严格的.但并非所有功能都是!在

f _ = 3
g x = x * 2
result = f (g x)
Run Code Online (Sandbox Code Playgroud)

它是不是该案件g x前必须计算f (g x).

  • @AlexisKing Laziness绝对是问题的核心:monads的使用在Haskell中变得流行,正是因为与严格的语言不同,懒惰的语言可以让一些东西完全没有评估,因此如果我们在懒惰的片段中留下"有效"的东西那些影响可能永远不会发生!Monads后来证明自己有用; 但最初,他们在那里解决问题_,我的观点是,这个问题正是在问为什么对这个问题的天真,纯粹的解决方案不够好. (3认同)