Monads,组成和计算顺序

时间:2016-12-24 04:37:51

标签: haskell monads composition

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

但是简单的构图怎么样? Ain&#t; t

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

result = f g x

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

monad是否做同样的事情但处理效果?

3 个答案:

答案 0 :(得分:13)

免责声明:Monads是很多东西。众所周知,它们难以解释,所以我不会试图解释一下一般的monad,因为这个问题没有要求。我将假设您已基本掌握Monad接口的内容以及它对某些有用数据类型的工作原理,例如MaybeEitherIO。< / p>

效果如何?

您的问题以便笺开头:

  

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

嗯。这是有趣的。实际上,有趣的原因有几个,其中一个原因已经确定:它暗示monads可以让你创建某种排序。这是真的,但它只是图片的一部分:表示测序发生在效果

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

错误效果

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

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

该示例的第二行由于Monad的{​​{1}}实例而“短路”。这可能被认为是一种影响吗?从某种意义上说,我是这么认为的,因为它是非本地的,但在另一种意义上,可能不是。毕竟,如果交换Maybex <- 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" 特定的定义替换>>=运算符,以完全摆脱monad:

Either

因此,很明显monad确实会进行某种排序,但这不是因为它们是monad而monad是神奇的,只是因为它们碰巧启用了一种看起来的编程风格不像Haskell通常允许的那样。

Monads和州

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

Writer

这比以前更有趣,因为现在> 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只是一个完全普通的Haskell数据类型,不执行任何I / O或类似的操作,但我们使用monadic接口创建看起来像有序效果的东西。

我们可以通过使用Writer类型创建一个看起来像可变状态的界面来进一步发展:

State

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

> flip execState 0 $ do
    modify (+ 3)
    modify (* 2)
6

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

为什么monad可以这样做?

是什么赋予了monads这种力量?好吧,它们不是魔术 - 它们只是普通的纯Haskell代码。但请考虑> flip execState 0 $ do modify (* 2) modify (+ 3) 3 的类型签名:

>>=

注意第二个参数如何依赖于(>>=) :: Monad m => m a -> (a -> m b) -> m b ,获得a的唯一方法是来自第一个参数?这意味着a需要“运行”第一个参数以生成值,之后它可以应用第二个参数。这与评估顺序没有关系,因为它与实际编写将要进行类型检查的代码有关。

现在,Haskell确实是一种懒惰的语言。但是Haskell的懒惰并不重要,因为所有这些代码实际上都是纯粹的,甚至是使用>>=的例子!它只是一种模式,它以纯粹的方式编码看起来有状态的计算,但是如果你自己实际实现了State,你会发现它只是绕过{{的定义中的“当前状态”。 1}}功能。没有任何实际的突变。

就是这样。 Monads凭借它们的接口,对如何评估它们的参数施加了一个排序,并且State的实例利用它来创建有状态的接口。但是,正如您所发现的那样, >>=不需要进行评估排序;显然在Monad中,将在乘法之前评估加法。

但是Monad ??

好的,你有我。问题在于:(1 + 2) * 3是神奇的。

Monad不是魔术,但IO是。以上所有示例都是纯粹的功能,但显然读取文件或写入stdout并不纯粹。那么IO如何工作呢?

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

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

但是,不要被误导!你不需要monad用纯语言做I / O,你不需要Monad来产生monadic效果。 Early versions of Haskell experimented with a non-monadic way to do I/O,这个答案的其他部分解释你怎么能有纯粹的monadic效果。请记住,monads不是特殊或神​​圣的,它们只是Haskell程序员因其各种属性而发现有用的模式。

答案 1 :(得分:6)

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

f _ = 3
g x = x * 2
result = f (g x)

<{1}}之前必须计算g x的情况。

答案 2 :(得分:2)

是的,monads使用函数组合来排序效果,并不是实现排序效果的唯一方法。

严格的语义和副作用

在大多数语言中,首先将严格语义应用于表达式的函数端,然后依次应用于每个参数,最后将该函数应用于参数。所以在JS中,函数应用程序表单

<Code 1>(<Code 2>, <Code 3>)

以指定的顺序运行四个代码:1,2,3,然后它检查1的输出是否为函数,然后用这两个计算的参数调用该函数。这样做是因为任何这些步骤都会产生副作用。你会写,

const logVal = (log, val) => {
  console.log(log);
  return val;
};
logVal(1, (a, b) => logVal(4, a+b))(
  logVal(2, 2),
  logVal(3, 3));

这适用于那些语言。这些是副作用,我们可以说在这个上下文中意味着JS的类型系统不会让你知道它们在那里。

Haskell确实有一个严格的应用程序原语,但它想要,这大致意味着它希望类型系统跟踪效果。因此,他们引入了一种元编程形式,其中一种类型是类型级形容词,“计算_____的程序”。一个程序与现实世界互动;理论上Haskell代码没有。您必须定义“main是一个计算单元类型的程序”,然后编译器实际上只是为您构建该程序作为可执行的二进制文件。当文件运行时 Haskell不再是真正的图片了!

因此,比普通函数应用程序更具体,因为我用JavaScript编写的抽象问题是,

  1. 我有一个程序,它计算{从(X,Y)对的函数到计算Zs的程序}。
  2. 我还有一个计算X的程序,以及一个计算Y的程序。
  3. 我想将这些全部放在一个计算Z的程序中。
  4. 那不是只是功能组合本身。但功能可以那个。

    偷看monad

    monad是一种模式。模式是,有时你有一个形容词,当你重复它时,它不会增加太多。例如,当您说“a 延迟延迟x”或“零或更多(零或更多xs)”或“无论是null还是null或x时,添加的内容都不多。 “类似地,对于IO monad,“用于计算计算x的程序的程序”并没有增加多少“在计算x的程序中不可用”。

    模式是有一些合并的规范合并算法:

      

    加入:给定<adjective> <adjective> x,我会为您<adjective> x

    我们还添加了两个其他属性,形容词应该是 outputtish

      

    地图:给定x -> y<adjective> x,我会为您<adjective> y

    普遍可嵌入

      

    纯:给定x,我会让你成为<adjective> x

    鉴于这三件事和一些公理,你碰巧有一个共同的“monad”想法,你可以为它开发一个真正的语法。

    现在这个元编程的想法显然包含了一个monad。在JS中我们会写,

    interface IO<x> {
      run: () => Promise<x>
    }
    function join<x>(pprog: IO<IO<x>>): IO<x> {
      return { run: () => pprog.run().then(prog => prog.run()) };
    }
    function map<x, y>(prog: IO<x>, fn: (in: x) => y): IO<y> {
      return { run: () => prog.run().then(x => fn(x)) }
    }
    function pure<x>(input: x): IO<x> {
      return { run: () => Promise.resolve(input) }
    }
    // with those you can also define,
    function bind<x, y>(prog: IO<x>, fn: (in: x) => IO<y>): IO<y> {
      return join(map(prog, fn));
    }
    

    但是存在模式的事实并不意味着它有用!我声称这些功能结果是你需要的所有来解决上面的问题。并且不难看出原因:您可以使用bind来创建一个不存在形容词的函数范围,并在那里操纵您的值:

    function ourGoal<x, y, z>(
      fnProg: IO<(inX: x, inY: y) => IO<z>>,
      xProg: IO<x>,
      yProg: IO<y>): IO<z> {
        return bind(fnProg, fn =>
          bind(xProg, x =>
            bind(yProg, y => fn(x, y))));
    }
    

    这是如何回答您的问题

    请注意,在上面我们通过编写三个bind的方式选择操作顺序。我们本可以用其他顺序写出来。但我们需要所有参数来运行最终程序。

    我们如何对操作进行排序的选择确实是在函数调用中实现你是100%正确。但是你这样做的方式,用< em> only 功能组合,是有缺陷的,因为它会将效果降级为副作用,以便通过它来获取类型。