在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
在最近简要回顾了Haskell之后,对于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”)允许您使用大括号和分号和/或缩进和换行。
其他回答
正如丹尼尔·斯皮瓦克(Daniel Spiewak)所解释的,修道院不是隐喻,而是从一种共同模式中产生的一种实用的抽象。
monad是用于封装状态变化的对象的东西。在不允许您具有可修改状态的语言(例如,Haskell)中最常遇到这种情况。
例如文件I/O。
您将能够使用文件I/O的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?
看到其他答案,似乎可以很自由地跳到细节中。
让下面的“{|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接口。