在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
当前回答
在Scala的上下文中,您会发现以下是最简单的定义。基本上,flatMap(或bind)是“关联”的,并且存在一个标识。
trait M[+A] {
def flatMap[B](f: A => M[B]): M[B] // AKA bind
// Pseudo Meta Code
def isValidMonad: Boolean = {
// for every parameter the following holds
def isAssociativeOn[X, Y, Z](x: M[X], f: X => M[Y], g: Y => M[Z]): Boolean =
x.flatMap(f).flatMap(g) == x.flatMap(f(_).flatMap(g))
// for every parameter X and x, there exists an id
// such that the following holds
def isAnIdentity[X](x: M[X], id: X => M[X]): Boolean =
x.flatMap(id) == x
}
}
E.g.
// These could be any functions
val f: Int => Option[String] = number => if (number == 7) Some("hello") else None
val g: String => Option[Double] = string => Some(3.14)
// Observe these are identical. Since Option is a Monad
// they will always be identical no matter what the functions are
scala> Some(7).flatMap(f).flatMap(g)
res211: Option[Double] = Some(3.14)
scala> Some(7).flatMap(f(_).flatMap(g))
res212: Option[Double] = Some(3.14)
// As Option is a Monad, there exists an identity:
val id: Int => Option[Int] = x => Some(x)
// Observe these are identical
scala> Some(7).flatMap(id)
res213: Option[Int] = Some(7)
scala> Some(7)
res214: Some[Int] = Some(7)
注:严格地说,函数编程中的Monad的定义与范畴理论中的Monard的定义不同,后者是按映射和展平的顺序定义的。尽管它们在某些映射下是等价的。这个演示非常好:http://www.slideshare.net/samthemonad/monad-presentation-scala-as-a-category
其他回答
解释monad似乎就像解释控制流语句一样。想象一下,一个非程序员要求你解释它们?
你可以给他们一个涉及理论的解释——布尔逻辑、寄存器值、指针、堆栈和框架。但那太疯狂了。
你可以用语法来解释它们。基本上,C中的所有控制流语句都有大括号,您可以通过它们相对于括号的位置来区分条件和条件代码。这可能更疯狂。
或者,您也可以解释循环、if语句、例程、子例程以及可能的协例程。
Monad可以取代相当多的编程技术。语言中有一种特定的语法支持它们,还有一些关于它们的理论。
它们也是函数式程序员使用命令式代码而不承认它的一种方式,但这并不是他们唯一的用途。
[免责声明:我仍在努力完全了解monads。以下是我目前所了解的情况。如果这是错误的,希望有有知识的人会在地毯上给我打电话。]
Arnar写道:
Monads只是一种包装东西的方法,它提供了对包装好的东西进行操作而不展开的方法。
正是这样。想法是这样的:
你需要一些价值,并用一些附加信息来包装它。就像值是某种类型的(例如整数或字符串)一样,附加信息也是某种类型的。例如,该额外信息可能是“可能”或“IO”。然后,您有一些运算符,允许您在携带附加信息的同时对打包的数据进行操作。这些运算符使用附加信息来决定如何更改包装值上的操作行为。例如,Maybe Int可以是Just Int或Nothing。现在,如果您将Maybe Int添加到Maybe Int,则运算符将检查它们是否都是内部的Just Int,如果是,则将展开Int,将其传递给加法运算符,将生成的Int重新包装为新的Just Int(这是有效的Maybe Int),从而返回Maybe Int。但如果其中一个是内部的Nothing,则该运算符将立即返回Nothing,这也是一个有效的Maybe Int。这样,你可以假装Maybe Ints只是正常的数字,并对它们进行常规运算。如果你得到了一个Nothing,你的方程仍然会产生正确的结果——而不必到处乱检查Nothing。
但这个例子正是Maybe所发生的事情。如果额外的信息是IO,那么将调用为IO定义的特殊运算符,并且在执行添加之前,它可以执行完全不同的操作。(好吧,将两个IO Int加在一起可能是荒谬的——我还不确定。)
基本上,“monad”大致意思是“模式”。但是,您现在有了一种语言构造(语法和所有),可以将新模式声明为程序中的东西,而不是一本充满了非正式解释和专门命名的模式的书。(这里的不精确之处在于所有模式都必须遵循特定的形式,因此monad不像模式那样通用。但我认为这是大多数人都知道和理解的最接近的术语。)
这就是为什么人们觉得单子如此令人困惑:因为它们是一个通用的概念。问是什么使某物成为monad与问是什么让某物成为模式类似。
但是想想在语言中对模式的概念提供语法支持的含义:你不必阅读“四人帮”一书,记住特定模式的构造,只需编写一次代码,以不可知的通用方式实现这个模式,然后就完成了!然后,您可以重用此模式,如Visitor或Strategy或Façade等,只需用它装饰代码中的操作,而无需反复重新实现它!
所以,这就是为什么理解monad的人会发现它们如此有用的原因:这并不是知识势利者以理解为荣的象牙塔概念(好吧,当然也是如此,teehee),而是实际上让代码更简单。
让下面的“{|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接口。
另一种尝试是解释monad,只使用Python列表和map函数。我完全接受这不是一个完整的解释,但我希望它能触及核心概念。
我从Monads上的funfunfunction视频和Learn You A Haskell章节“为了几个Monads更多”中得到了这一点的基础。我强烈推荐观看funfunfunction视频。
最简单的是,Monad是具有map和flatMap函数(在Haskell中绑定)的对象。有一些额外的必需财产,但这些是核心属性。
flatMap“展平”map的输出,对于列表,这只是连接列表的值,例如。
concat([[1], [4], [9]]) = [1, 4, 9]
因此,在Python中,我们基本上可以通过以下两个函数实现Monad:
def flatMap(func, lst):
return concat(map(func, lst))
def concat(lst):
return sum(lst, [])
func是任何接受值并返回列表的函数。
lambda x: [x*x]
解释
为了清楚起见,我通过一个简单的函数在Python中创建了concat函数,该函数将列表相加,即[]+[1]+[4]+[9]=[1,4,9](Haskell有一个原生的concat方法)。
我假设你知道地图功能是什么,例如:
>>> list(map(lambda x: [x*x], [1,2,3]))
[[1], [4], [9]]
展平是Monad的关键概念,对于每个作为Monad的对象,这种展平允许您获得Monad中包裹的值。
现在我们可以呼叫:
>>> flatMap(lambda x: [x*x], [1,2,3])
[1, 4, 9]
这个lambda取一个值x并将其放入一个列表中。monad适用于从值到monad类型的任何函数,所以在本例中是列表。
这是你的monad定义。
我认为为什么它们有用的问题已经在其他问题中得到了回答。
更多说明
其他不是列表的例子有JavaScript Promise,它有then方法,JavaScript Streams有flatMap方法。
因此Promise和Streams使用了一个稍微不同的函数,它将Stream或Promise展平,并从内部返回值。
Haskell列表monad具有以下定义:
instance Monad [] where
return x = [x]
xs >>= f = concat (map f xs)
fail _ = []
即有三个函数return(不要与大多数其他语言中的return混淆)、>>=(flatMap)和fail。
希望您能看到以下两者之间的相似之处:
xs >>= f = concat (map f xs)
and:
def flatMap(f, xs):
return concat(map(f, xs))
这个答案从一个激励性的例子开始,通过这个例子,得出一个单子的例子,并正式定义了“单子”。
考虑伪代码中的这三个函数:
f(<x, messages>) := <x, messages "called f. ">
g(<x, messages>) := <x, messages "called g. ">
wrap(x) := <x, "">
f采用<x,messages>形式的有序对,并返回一个有序对。它保持第一项不变,并在第二项后面附加“called f.”。与g相同。
您可以组合这些函数并获得原始值,以及显示函数调用顺序的字符串:
f(g(wrap(x)))
= f(g(<x, "">))
= f(<x, "called g. ">)
= <x, "called g. called f. ">
您不喜欢f和g负责将自己的日志消息附加到先前的日志信息。(为了论证起见,想象一下,f和g必须对这对中的第二项执行复杂的逻辑,而不是附加字符串。在两个或多个不同的函数中重复这种复杂的逻辑会很痛苦。)
您更喜欢编写更简单的函数:
f(x) := <x, "called f. ">
g(x) := <x, "called g. ">
wrap(x) := <x, "">
但看看当你编写它们时会发生什么:
f(g(wrap(x)))
= f(g(<x, "">))
= f(<<x, "">, "called g. ">)
= <<<x, "">, "called g. ">, "called f. ">
问题是,将一对传递到函数中并不能得到所需的结果。但如果你可以将一对输入到函数中呢:
feed(f, feed(g, wrap(x)))
= feed(f, feed(g, <x, "">))
= feed(f, <x, "called g. ">)
= <x, "called g. called f. ">
将feed(f,m)读为“feed m into f”。要将一对<x,messages>输入函数f,需要将x传递给f,从f中获取<y,messages〕,并返回<y,message message>。
feed(f, <x, messages>) := let <y, message> = f(x)
in <y, messages message>
请注意,当您对函数执行三项操作时会发生什么:
首先:如果包装一个值,然后将结果对送入函数:
feed(f, wrap(x))
= feed(f, <x, "">)
= let <y, message> = f(x)
in <y, "" message>
= let <y, message> = <x, "called f. ">
in <y, "" message>
= <x, "" "called f. ">
= <x, "called f. ">
= f(x)
这与将值传递给函数相同。
第二:如果你把一对放进包装里:
feed(wrap, <x, messages>)
= let <y, message> = wrap(x)
in <y, messages message>
= let <y, message> = <x, "">
in <y, messages message>
= <x, messages "">
= <x, messages>
这不会改变这对。
第三:如果定义了一个函数,该函数将x和g(x)输入f:
h(x) := feed(f, g(x))
并向其中输入一对:
feed(h, <x, messages>)
= let <y, message> = h(x)
in <y, messages message>
= let <y, message> = feed(f, g(x))
in <y, messages message>
= let <y, message> = feed(f, <x, "called g. ">)
in <y, messages message>
= let <y, message> = let <z, msg> = f(x)
in <z, "called g. " msg>
in <y, messages message>
= let <y, message> = let <z, msg> = <x, "called f. ">
in <z, "called g. " msg>
in <y, messages message>
= let <y, message> = <x, "called g. " "called f. ">
in <y, messages message>
= <x, messages "called g. " "called f. ">
= feed(f, <x, messages "called g. ">)
= feed(f, feed(g, <x, messages>))
这与将对输入g和将所得对输入f相同。
你有大部分的单子。现在您只需要了解程序中的数据类型。
<x,“称为f”>是什么类型的值?这取决于x是什么类型的值。如果x是t类型的,那么你的对就是“t和字符串对”类型的值了。称之为M型。
M是一个类型构造器:M本身并不表示一个类型,但一旦你用一个类型填空,M _就表示一个。M int是一对int和一个字符串。M字符串是一对字符串和一个字符串。等
恭喜你,你已经创建了monad!
形式上,你的monad是元组<M,feed,wrap>。
monad是一个元组<M,feed,wrap>,其中:
M是类型构造函数。feed接受一个(函数接受一个t并返回一个M u)和一个M t并返回M u。wrap接受一个v并返回一个M v。
t、 u和v是可以相同也可以不同的任意三种类型。单子满足您为特定单子证明的三个财产:
将包裹的t送入函数与将未包裹的t传入函数相同。形式上:饲料(f,包装(x))=f(x)将M t喂入包装物对M t没有任何影响。形式上:进给(包裹,m)=m将一个M t(称为M)输入一个函数将t传递到g从g得到一个M u(称为n)将n输入f与m进g从g得到n将n输入f形式上:饲料(h,m)=饲料(f,饲料(g,m)),其中h(x):=饲料(f,g(x))
通常,feed称为bind(在Haskell中为AKA>>=),wrap称为return。