haskell - 单声道,组成和计算顺序

标签 haskell monads composition

所有monad文章经常声明,monad允许您按顺序排列效果。

但是简单的组成呢?不会

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

result = f g x


需要在g x之前计算f ...

monad可以做到一样但可以处理效果吗?

最佳答案

免责声明:Monad有很多东西。众所周知,它们很难解释,因此,在此我将不尝试解释一般的monad,因为这个问题并不需要。我将假定您对Monad接口及其对某些有用的数据类型(如MaybeEitherIO)的工作方式有基本的了解。



有什么作用?

您的问题以一个音符开头:


所有monad文章经常声明,monad允许您按顺序排列效果。


嗯这是有趣的。实际上,有几个原因很有趣,您已经确定了其中一个原因:这意味着单子可以让您创建某种排序。没错,但这只是图片的一部分:它还指出排序发生在效果上。

不过,这就是……什么是“效果”?将两个数字相加会产生效果吗?根据大多数定义,答案是否定的。打印一些东西到stdout会怎么样?在这种情况下,我想大多数人都会同意答案是肯定的。但是,请考虑一些更微妙的问题:通过产生Nothing效应来使计算短路吗?

错误影响

让我们来看一个例子。考虑以下代码:

> do x <- Just 1
     y <- Nothing
     return (x + y)
Nothing


该示例的第二行由于MonadMaybe实例而导致“短路”。可以认为是效果吗?从某种意义上说,我认为是这样,因为它不是本地的,但从另一种意义上来说,可能不是。毕竟,如果交换x <- Just 1y <- 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/

相关文章:

haskell - 理解 Haskell 中 Monad 绑定(bind)运算符的关联性

class - haskell : Working with monad classes

haskell - 使用来自 REPL 的 Monadic eDSL

javascript - 如何在组合函数中传递泛型,而不是简单类型(例如 : string, 数字)

javascript - ES6 类中的访问器组合

scala - 理解产品和副产品的图表

java - 如何使用 Monadic Bind 简化此 Apache Tomcat 代码?

c# - MEF RegistrationBuilder 导出特定接口(interface)实现

windows - 如何在Windows中安装haskell openid包

haskell - 为什么 Haskell 中是 (=<<) id = join ?