在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
当前回答
在几年前回答了这个问题之后,我相信我可以通过。。。
monad是一种函数组合技术,它使用组合函数bind将某些输入场景的处理具体化,以在组合过程中预处理输入。
在正常合成中,函数compose(>>)用于按顺序将合成的函数应用于其前身的结果。重要的是,所组成的函数需要处理其输入的所有场景。
(x->y)>>(y->z)
这种设计可以通过重组输入来改进,以便更容易地询问相关状态。因此,如果y包含有效性的概念,则值可以变成Mb,例如(is_OK,b),而不是简单的y。
例如,当输入仅可能是一个数字时,而不是返回一个可以尽职尽责地包含数字或不包含数字的字符串,您可以将类型重新构造为bool,以指示元组中存在有效数字和数字,例如bool*float。组合函数现在不再需要解析输入字符串来确定数字是否存在,而只需要检查元组的布尔部分。
(Ma->Mb)>>(Mb->Mc)
在这里,合成与合成一起自然发生,因此每个函数必须单独处理其输入的所有场景,尽管现在这样做要容易得多。
然而,如果我们能够将审讯的工作外化,以应对那些处理场景是常规的情况,那又会怎样呢。例如,如果我们的程序在输入不正常时什么都不做,比如is_OK为false时。如果做到了这一点,那么组合函数就不需要自己处理该场景,从而大大简化了代码并实现了另一个级别的重用。
为了实现这种外部化,我们可以使用bind(>>=)函数来执行组合而不是组合。因此,不是简单地将值从一个函数的输出传递到另一个函数输入,而是检查Ma的M部分,并决定是否以及如何将组合函数应用于a。当然,函数绑定将专门为我们的特定M定义,以便能够检查其结构并执行我们想要的任何类型的应用。尽管如此,a可以是任何东西,因为bind仅在确定应用程序需要时将未检查的a传递给组合函数。此外,组合函数本身也不再需要处理输入结构的M部分,从而简化了它们。因此
(a->Mb)>>=(b->Mc)或更简洁地Mb>>=
简言之,一旦输入被设计为充分暴露某些输入场景,monad就外部化了,从而提供了关于处理这些输入场景的标准行为。这种设计是一种外壳和内容模型,其中外壳包含与组合函数的应用程序相关的数据,并由绑定函数查询,并且仅对绑定函数可用。
因此,单子是三件事:
M外壳,用于保存monad相关信息,实现的绑定函数,用于在将组合函数应用于其在外壳中找到的内容值时使用该外壳信息,以及形式为a->Mb的可组合函数,生成包含单元管理数据的结果。
一般来说,函数的输入比其输出更具限制性,其中可能包括错误条件等;因此,Mb结果结构通常非常有用。例如,当除数为0时,除法运算符不返回数字。
此外,monad可以包括将值a包装成monadic类型Ma的包装函数,以及将一般函数a->b包装成monodic函数a->Mb的包装函数。当然,像bind一样,这样的包装函数是M特有的。例如:
let return a = [a]
let lift f a = return (f a)
绑定函数的设计假定了不可变的数据结构和纯函数,其他事情变得复杂,无法保证。因此,有一元定律:
鉴于
M_
return = (a -> Ma)
f = (a -> Mb)
g = (b -> Mc)
然后
Left Identity : (return a) >>= f === f a
Right Identity : Ma >>= return === Ma
Associative : Ma >>= (f >>= g) === Ma >>= ((fun x -> f x) >>= g)
关联性意味着无论何时应用绑定,绑定都会保留求值顺序。也就是说,在上述关联性的定义中,对f和g的括号化绑定的强制早期评估只会导致期望Ma的函数完成绑定。因此,必须先确定Ma的值,然后才能将其值应用于f,进而将结果应用于g。
其他回答
Monad用于控制流,就像抽象数据类型用于数据一样。
换句话说,许多开发人员对集合、列表、字典(或哈希、或地图)和树的概念很熟悉。在这些数据类型中有许多特殊情况(例如InsertionOrderPreservingIdentityHashMap)。
然而,当面对程序“流”时,许多开发人员还没有接触到比if、switch/case、do、while、goto(grr)和(可能)闭包更多的构造。
因此,monad只是一个控制流构造。替代monad的更好短语是“控制类型”。
因此,monad具有用于控制逻辑、语句或函数的槽——数据结构中的等价物是,某些数据结构允许您添加数据,并删除数据。
例如,“if”monad:
if( clause ) then block
最简单的是有两个槽:一个子句和一个块。if monad通常用于评估子句的结果,如果不是false,则评估块。许多开发人员在学习“如果”时并没有接触到monad,而且编写有效的逻辑并不需要理解monad。
monad可能会变得更复杂,就像数据结构可能变得更复杂一样,但monad有很多大类可能具有相似的语义,但实现和语法不同。
当然,数据结构可以在单子上迭代或遍历,也可以以同样的方式进行评估。
编译器可能支持也可能不支持用户定义的monad。哈斯克尔当然知道。Ioke有一些类似的功能,尽管语言中没有使用monad一词。
我也在努力理解单子。这是我的版本:
Monad是关于对重复的事物进行抽象的。首先,monad本身是一个类型化接口(像抽象泛型类),它有两个函数:bind和return,它们定义了签名。然后,我们可以基于抽象的monad创建具体的monad,当然还有绑定和返回的具体实现。此外,绑定和返回必须满足几个不变量,以便可以组合/链接具体的单体。
当我们有接口、类型、类和其他工具来创建抽象时,为什么要创建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接口。
monad实际上是“类型运算符”的一种形式。它将做三件事。首先,它会将一种类型的值“包装”(或以其他方式转换)为另一种类型(通常称为“一元类型”)。第二,它将使底层类型上的所有操作(或函数)在monadic类型上可用。最后,它将为将自身与另一个monad组合以生成复合monad提供支持。
“可能monad”本质上等同于Visual Basic/C#中的“可为null的类型”。它接受不可为null的类型“T”并将其转换为“可为null<T>”,然后定义所有二进制运算符在可为null><T>上的含义。
副作用也有类似的表现。创建了一个结构,该结构包含函数返回值旁边的副作用描述。当值在函数之间传递时,“提升”操作会复制副作用。
它们被称为“monad”,而不是更容易理解的“类型运算符”的名称,原因如下:
Monad对他们的行为有限制(详见定义)。这些限制,加上涉及三个运算,符合范畴理论中一个叫做monad的结构,这是一个模糊的数学分支。它们是由“纯”函数语言的支持者设计的纯函数语言的支持者,如模糊的数学分支由于数学晦涩难懂,而且monad与特定的编程风格相关,人们倾向于使用monad这个词作为一种秘密握手。正因为如此,没有人费心去投资一个更好的名字。
在Coursera“反应式编程原理”培训中,Erik Meier将其描述为:
"Monads are return types that guide you through the happy path." -Erik Meijer