在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?

我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。


当前回答

monad实际上是“类型运算符”的一种形式。它将做三件事。首先,它会将一种类型的值“包装”(或以其他方式转换)为另一种类型(通常称为“一元类型”)。第二,它将使底层类型上的所有操作(或函数)在monadic类型上可用。最后,它将为将自身与另一个monad组合以生成复合monad提供支持。

“可能monad”本质上等同于Visual Basic/C#中的“可为null的类型”。它接受不可为null的类型“T”并将其转换为“可为null<T>”,然后定义所有二进制运算符在可为null><T>上的含义。

副作用也有类似的表现。创建了一个结构,该结构包含函数返回值旁边的副作用描述。当值在函数之间传递时,“提升”操作会复制副作用。

它们被称为“monad”,而不是更容易理解的“类型运算符”的名称,原因如下:

Monad对他们的行为有限制(详见定义)。这些限制,加上涉及三个运算,符合范畴理论中一个叫做monad的结构,这是一个模糊的数学分支。它们是由“纯”函数语言的支持者设计的纯函数语言的支持者,如模糊的数学分支由于数学晦涩难懂,而且monad与特定的编程风格相关,人们倾向于使用monad这个词作为一种秘密握手。正因为如此,没有人费心去投资一个更好的名字。

其他回答

经过努力,我想我终于明白了单子。在重新阅读了我自己对绝大多数投票结果的冗长批评之后,我将给出这个解释。

要理解单子,需要回答三个问题:

你为什么需要蒙纳德?什么是单子?如何实现monad?

正如我在最初的评论中所指出的,有太多的monad解释被第3个问题所困扰,没有,也没有充分地涵盖第2个问题或第1个问题。

你为什么需要蒙纳德?

Haskell等纯函数式语言与C或Java等命令式语言的不同之处在于,纯函数式程序不一定按特定顺序执行,一步一步执行。Haskell程序更类似于一个数学函数,在该函数中,您可以以任意数量的潜在阶数求解“方程”。这带来了许多好处,其中之一是它消除了某些类型的错误的可能性,特别是那些与“状态”相关的错误。

然而,使用这种编程风格,有些问题不是很容易解决的。有些事情,比如控制台编程和文件i/o,需要按照特定的顺序进行,或者需要维护状态。处理这个问题的一种方法是创建一种表示计算状态的对象,以及一系列将状态对象作为输入并返回新修改的状态对象的函数。

因此,让我们创建一个假设的“状态”值,它表示控制台屏幕的状态。这个值是如何构造的并不重要,但假设它是一个字节长度的ascii字符数组,表示屏幕上当前可见的内容,以及一个表示用户输入的最后一行伪代码的数组。我们已经定义了一些接受控制台状态、修改它并返回新控制台状态的函数。

consolestate MyConsole = new consolestate;

因此,要进行控制台编程,但以纯函数的方式,您需要在彼此之间嵌套许多函数调用。

consolestate FinalConsole = print(input(print(myconsole, "Hello, what's your name?")),"hello, %inputbuffer%!");

以这种方式编程保持了“纯”的功能风格,同时强制对控制台的更改按特定顺序进行。但是,我们可能希望像上面的示例一样,一次只执行几个操作。以这种方式嵌套函数将开始变得笨拙。我们想要的是基本上与上面相同的代码,但编写得更像这样:

consolestate FinalConsole = myconsole:
                            print("Hello, what's your name?"):
                            input():
                            print("hello, %inputbuffer%!");

这确实是一种更方便的写法。但我们如何做到这一点呢?

什么是单子?

一旦你定义了一个类型(比如consoleestate),以及一系列专门为该类型操作而设计的函数,你就可以通过定义一个操作符(比如:(bind))将这些东西的整个包变成一个“monad”,该操作符会自动将返回值输入到左边的函数参数中,转换为与特定类型的绑定运算符一起工作的函数。

如何实现monad?

看到其他答案,似乎可以很自由地跳到细节中。

公主对F#计算表达式的解释帮助了我,尽管我仍然不能说我真的理解了。

编辑:这个系列-用javascript解释monad-对我来说是一个“打破平衡”的系列。

http://blog.jcoglan.com/2011/03/05/translation-from-haskell-to-javascript-of-selected-portions-of-the-best-introduction-to-monads-ive-ever-read/http://blog.jcoglan.com/2011/03/06/monad-syntax-for-javascript/http://blog.jcoglan.com/2011/03/11/promises-are-the-monad-of-asynchronous-programming/

我认为理解单子是一件让你毛骨悚然的事。从这个意义上说,尽可能多地阅读“教程”是一个好主意,但通常奇怪的东西(不熟悉的语言或语法)会让你的大脑无法专注于基本内容。

有些事情我很难理解:

基于规则的解释对我来说从未奏效,因为大多数实际示例实际上需要的不仅仅是返回/绑定。此外,称之为规则也无济于事。这更像是“有些东西有共同点,我们把它们称为‘单子’,把共同点称为‘规则’”。Return(a->M<a>)和Bind(M<a>->(a->M<b>)->M<b>)很好,但我永远无法理解Bind如何从M<a>中提取a,以便将其传递给a->M<b>。我不认为我在任何地方读过(也许这对其他人来说都很明显),Return(M<a>->a)的反面必须存在于monad内部,它只是不需要暴露。

根据我们所谈论的monad,“什么是monad”这个问题是错误的:

对“什么是单单体?”这个问题的简短回答是,它是内函子范畴中的单单体,或者它是一种通用数据类型,配备了满足某些定律的两个运算。这是正确的,但它并没有揭示一个重要的大局。这是因为问题是错误的。在这篇论文中,我们的目标是回答正确的问题,即“当作者谈论单子时,他们真正说的是什么?”

虽然这篇论文没有直接回答什么是单子,但它有助于理解不同背景的人谈论单子时的含义以及原因。

我最喜欢的Monad教程:

http://www.haskell.org/haskellwiki/All_About_Monads

(在谷歌搜索“monad教程”的17万次点击中!)

@斯图:monads的目的是允许您将(通常)顺序语义添加到纯代码中;您甚至可以组合Monad(使用Monad Transformers)并获得更有趣和复杂的组合语义,例如,带有错误处理的解析、共享状态和日志记录。所有这些在纯代码中都是可能的,monad只允许您将其抽象并在模块化库中重用(在编程中总是很好的),并提供方便的语法使其看起来势在必行。

Haskell已经有了运算符重载[1]:它使用类型类的方式与使用Java或C#中的接口的方式非常相似,但Haskell恰好也允许使用非字母数字标记(如+&&和>)作为中缀标识符。如果您的意思是“重载分号”[2],那么在您看来这只是运算符重载。“重载分号”听起来像是黑魔法,自找麻烦(想象一下有进取心的Perl黑客听到了这个想法),但关键是没有monad就没有分号,因为纯函数代码不需要或不允许显式排序。

这一切听起来比实际情况要复杂得多。sigfpe的文章很酷,但使用了Haskell来解释它,这有点无法打破理解Haskell到grok Monads和理解Monads到grok Haskell的鸡和蛋的问题。

[1] 这是与monad不同的问题,但monad使用Haskell的运算符重载特性。

[2] 这也是一个过度简化,因为链接一元操作的运算符是>>=(发音为“bind”),但有语法糖(“do”)允许您使用大括号和分号和/或缩进和换行。

让下面的“{|a|m}”表示一些一元数据。宣传以下内容的数据类型:

        (I got an a!)
          /        
    {| a |m}

函数f知道如何创建monad,只要它有一个a:

       (Hi f! What should I be?)
                      /
(You?. Oh, you'll be /
 that data there.)  /
 /                 /  (I got a b.)
|    --------------      |
|  /                     |
f a                      |
  |--later->       {| b |m}

在这里,我们看到函数f试图评估monad,但遭到了谴责。

(Hmm, how do I get that a?)
 o       (Get lost buddy.
o         Wrong type.)
o       /
f {| a |m}

函数f通过使用>>=找到提取a的方法。

        (Muaahaha. How you 
         like me now!?)       
    (Better.)      \
        |     (Give me that a.)
(Fine, well ok.)    |
         \          |
   {| a |m}   >>=   f

殊不知,monad和>>=勾结在一起。

            (Yah got an a for me?)       
(Yeah, but hey    | 
 listen. I got    |
 something to     |
 tell you first   |
 ...)   \        /
         |      /
   {| a |m}   >>=   f

但他们实际上在谈论什么?嗯,这取决于单子。仅仅抽象地谈论用处有限;你必须对特定的单子有一些经验,才能充实理解。

例如,数据类型Maybe

 data Maybe a = Nothing | Just a

有一个monad实例,其行为如下。。。

其中,如果情况只是

            (Yah what is it?)       
(... hm? Oh,      |
forget about it.  |
Hey a, yr up.)    | 
            \     |
(Evaluation  \    |
time already? \   |
Hows my hair?) |  |
      |       /   |
      |  (It's    |
      |  fine.)  /
      |   /     /    
   {| a |m}   >>=   f

但对于Nothing的情况

        (Yah what is it?)       
(... There      |
is no a. )      |
  |        (No a?)
(No a.)         |
  |        (Ok, I'll deal
  |         with this.)
   \            |
    \      (Hey f, get lost.) 
     \          |   ( Where's my a? 
      \         |     I evaluate a)
       \    (Not any more  |
        \    you don't.    |
         |   We're returning
         |   Nothing.)   /
         |      |       /
         |      |      /
         |      |     /
   {| a |m}   >>=   f      (I got a b.)
                    |  (This is   \
                    |   such a     \
                    |   sham.) o o  \
                    |               o|
                    |--later-> {| b |m}

因此,如果Maye monad实际上包含它所宣传的a,则它允许计算继续,但如果不包含,则中止计算。然而,结果仍然是一段单元数据,尽管不是f的输出。因此,Maye monad用于表示失败的上下文。

不同的单子叶植物表现不同。列表是具有一元实例的其他类型的数据。它们的行为如下:

(Ok, here's your a. Well, its
 a bunch of them, actually.)
  |
  |    (Thanks, no problem. Ok
  |     f, here you go, an a.)
  |       |
  |       |        (Thank's. See
  |       |         you later.)
  |  (Whoa. Hold up f,      |
  |   I got another         |
  |   a for you.)           |
  |       |      (What? No, sorry.
  |       |       Can't do it. I 
  |       |       have my hands full
  |       |       with all these "b" 
  |       |       I just made.) 
  |  (I'll hold those,      |
  |   you take this, and   /
  |   come back for more  /
  |   when you're done   / 
  |   and we'll do it   / 
  |   again.)          /
   \      |  ( Uhhh. All right.)
    \     |       /    
     \    \      /
{| a |m}   >>=  f  

在这种情况下,该函数知道如何从其输入生成列表,但不知道如何处理额外的输入和额外的列表。bind>>=,通过组合多个输出帮助f。我通过这个例子来说明,当>>=负责提取a时,它也可以访问f的最终绑定输出。事实上,除非它知道最终输出具有相同类型的上下文,否则它永远不会提取任何a。

还有其他monad用于表示不同的上下文。下面是一些其他特征。IO monad实际上没有a,但它认识一个人,会为你拿到a。州立大学圣莫尼德分校有一个秘密的圣莫尼德,它会把圣莫尼德藏在桌子下面给f,尽管f只是来要求一个a。

所有这一切的关键是,任何类型的数据如果声明自己是Monad,都会声明某种上下文来从Monad中提取值。从这一切中获得的巨大收益?好吧,用某种上下文来进行计算是很容易的。然而,当将多个上下文负载的计算串联在一起时,可能会变得混乱。monad操作负责解决上下文的交互,因此程序员不必这样做。

注意,>>=的使用通过从f中移除一些自主权来缓解混乱。也就是说,例如,在上面的Nothing情况下,f不再能够决定在Nothing的情况下要做什么;它被编码为>>=。这就是权衡。如果f有必要决定在Nothing的情况下做什么,那么f应该是从Maybe a到Maybe b的函数。在这种情况下,也许是monad是无关紧要的。

然而,请注意,有时数据类型不会导出它的构造函数(看看你的IO),如果我们想使用广告值,我们别无选择,只能使用它的monadic接口。