在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
当前回答
对于来自命令式背景(c#)的人,
考虑以下代码
bool ReturnTrueorFalse(SomeObject input)
{
if(input.Property1 is invalid)
{
return false;
}
if(input.Property2 is invalid)
{
return false;
}
DoSomething();
return true;
}
您会看到很多这样的代码,甚至不会看到早期返回,但所有检查都是嵌套完成的。现在,Monad是一种模式,它可以像下面一样被压平
Monad<bool> ReturnTrueorFalse(SomeObject input) =>
from isProperty1Valid in input.Property1
from isProperty2Valid in input.Property2
select Monad.Create(isProperty1Valid && isProperty2Valid);
这里有几点需要注意。首先,更改函数的返回值。其次,输入的两个财产都必须是Monad。接下来,Monad应该实现SelectMany(LINQ的展平运算符)。由于SelectMany是为该类型实现的,因此可以使用查询语法编写语句
那幺,什么是莫纳德?它是一种以可组合方式对返回相同类型的表达式进行扁平化的结构。这在函数式编程中特别有用,因为大多数函数式应用程序倾向于将状态和IO保持在应用程序的边缘层(例如:控制器),并在整个调用堆栈中返回基于Monad的返回值,直到需要解包该值。当我第一次看到这张照片时,我最大的优点是它很容易在眼睛上看到,也很有陈腔滥调。
每个c#(现在几乎每个人)开发人员都能立即识别的Monad的最佳示例是async/await。在.Net4.5之前,我们必须使用ContinueWith编写基于任务的语句来处理回调,在async/await之后,我们开始使用同步语法来处理异步语法。这是可能的,因为Task是一个“monad”。
关于OOP开发人员的详细说明,请参阅本文,这是一个简单的实现和语言文本,其中包含许多很棒的Monad和大量关于函数式编程的信息
其他回答
第一:如果你不是数学家,monad这个词有点空洞。另一个术语是计算构建器,它更能描述它们的实际用途。
它们是链接操作的模式。它看起来有点像面向对象语言中的方法链接,但机制略有不同。
该模式主要用于函数式语言(特别是Haskell,它普遍使用monad),但也可以用于支持高阶函数的任何语言(即可以将其他函数作为参数的函数)。
JavaScript中的数组支持该模式,因此让我们将其作为第一个示例。
模式的要点是我们有一个类型(在本例中为Array),它有一个以函数作为参数的方法。提供的操作必须返回相同类型的实例(即返回数组)。
首先是一个不使用monad模式的方法链接示例:
[1,2,3].map(x => x + 1)
结果是[2,3,4]。代码不符合monad模式,因为我们作为参数提供的函数返回的是数字,而不是数组。monad形式的相同逻辑是:
[1,2,3].flatMap(x => [x + 1])
这里我们提供了一个返回Array的操作,所以现在它符合模式。flatMap方法为数组中的每个元素执行提供的函数。它期望每个调用都有一个数组作为结果(而不是单个值),但将得到的数组集合并为一个数组。所以最终的结果是相同的,数组[2,3,4]。
(提供给map或flatMap等方法的函数参数在JavaScript中通常称为“回调”。我将其称为“操作”,因为它更通用。)
如果我们连锁多个操作(以传统方式):
[1,2,3].map(a => a + 1).filter(b => b != 3)
数组中的结果[2,4]
monad形式的相同链接:
[1,2,3].flatMap(a => [a + 1]).flatMap(b => b != 3 ? [b] : [])
产生相同的结果,即数组[2,4]。
您将立即注意到monad格式比非monad格式更难看!这正好表明单子不一定“好”。它们是一种有时有益有时不有益的模式。
请注意,monad模式可以以不同的方式组合:
[1,2,3].flatMap(a => [a + 1].flatMap(b => b != 3 ? [b] : []))
这里的绑定是嵌套的,而不是链式的,但结果是一样的。这是单子的一个重要属性,我们稍后会看到。这意味着组合的两个操作可以被视为单个操作。
该操作允许返回具有不同元素类型的数组,例如,将数字数组转换为字符串数组或其他东西;只要它仍然是一个数组。
这可以使用Typescript表示法更正式地描述。数组的类型为array<T>,其中T是数组中元素的类型。flatMap()方法接受类型为T=>Array<U>的函数参数,并返回一个Array<U>。
一般来说,monad是任何类型的Foo<Bar>,它有一个“bind”方法,该方法接受类型为Bar=>Foo<Baz>的函数参数,并返回一个Foo<Baz>。
这回答了单子是什么。这个答案的其余部分将试图通过示例来解释为什么monads在Haskell这样的语言中是一种有用的模式,而Haskell对monads有很好的支持。
Haskell和Do表示法
要将map/filter示例直接转换为Haskell,我们将flatMap替换为>>=运算符:
[1,2,3] >>= \a -> [a+1] >>= \b -> if b == 3 then [] else [b]
>>=运算符是Haskell中的绑定函数。当操作数是一个列表时,它与JavaScript中的flatMap相同,但对于其他类型,它被重载了不同的含义。
但是Haskell还为monad表达式提供了专用语法do块,它完全隐藏了绑定运算符:
do a <- [1,2,3]
b <- [a+1]
if b == 3 then [] else [b]
这将隐藏“管道”,并让您专注于在每个步骤中应用的实际操作。
在do块中,每一行都是一个操作。约束仍然认为块中的所有操作都必须返回相同的类型。因为第一个表达式是一个列表,所以其他操作也必须返回一个列表。
向后箭头<-看起来像赋值,但请注意,这是绑定中传递的参数。因此,当右侧的表达式是整数列表时,左侧的变量将是一个整数,但将对列表中的每个整数执行。
示例:安全导航(Maybe类型)
关于列表,让我们来看看monad模式如何对其他类型有用。
某些函数可能不总是返回有效值。在Haskell中,这由Maybe类型表示,该类型是Just value或Nothing选项。
总是返回有效值的链接操作当然很简单:
streetName = getStreetName (getAddress (getUser 17))
但如果任何函数都可以返回Nothing呢?我们需要单独检查每个结果,如果不是Nothing,则只将值传递给下一个函数:
case getUser 17 of
Nothing -> Nothing
Just user ->
case getAddress user of
Nothing -> Nothing
Just address ->
getStreetName address
很多重复检查!想象一下如果链条更长。Haskell用Maybe的monad模式解决了这个问题:
do
user <- getUser 17
addr <- getAddress user
getStreetName addr
这个do块调用Maybe类型的绑定函数(因为第一个表达式的结果是Maybe)。绑定函数仅在值为Just值时执行以下操作,否则只传递Nothing。
这里使用monad模式来避免重复代码。这与其他一些语言使用宏来简化语法的方式类似,尽管宏以非常不同的方式实现了相同的目标。
请注意,Haskell中monad模式和monad友好语法的结合导致了代码更干净。在JavaScript这样的语言中,如果没有对monad的任何特殊语法支持,我怀疑monad模式是否能够在这种情况下简化代码。
可变状态
Haskell不支持可变状态。所有变量都是常量,所有值都是不可变的。但State类型可用于模拟具有可变状态的编程:
add2 :: State Integer Integer
add2 = do
-- add 1 to state
x <- get
put (x + 1)
-- increment in another way
modify (+1)
-- return state
get
evalState add2 7
=> 9
add2函数构建一个monad链,然后以7作为初始状态对其求值。
显然,这在Haskell中才有意义。其他语言支持开箱即用的可变状态。Haskell通常在语言特性上是“选择加入”的——您可以在需要时启用可变状态,并且类型系统确保效果是显式的。IO是这方面的另一个例子。
IO
IO类型用于链接和执行“不纯”函数。
与任何其他实用语言一样,Haskell有一系列与外界接口的内置函数:putStrLine、readLine等。这些函数被称为“不纯”,因为它们要么会产生副作用,要么会产生不确定性的结果。即使是像获取时间这样简单的事情也被认为是不纯洁的,因为结果是不确定的——用相同的参数调用两次可能会返回不同的值。
纯函数是确定性的——它的结果完全取决于传递的参数,除了返回值之外,它对环境没有任何副作用。
Haskell大力鼓励使用纯函数——这是该语言的一个主要卖点。不幸的是,对于纯粹主义者来说,你需要一些不纯的函数来做任何有用的事情。Haskell折衷方案是将纯函数和不纯函数彻底分开,并保证纯函数无法直接或间接执行不纯函数。
这是通过给所有不纯函数赋予IO类型来保证的。Haskell程序的入口点是具有IO类型的主函数,因此我们可以在顶层执行不纯的函数。
但是该语言如何防止纯函数执行不纯函数?这是因为Haskell的懒惰本性。只有当某个函数的输出被其他函数消耗时,才执行该函数。但除了将IO值分配给main之外,没有办法使用它。因此,如果一个函数想要执行一个不纯的函数,它必须连接到main并具有IO类型。
对IO操作使用monad链接还可以确保它们以线性和可预测的顺序执行,就像命令式语言中的语句一样。
这让我们看到大多数人会用Haskell编写的第一个程序:
main :: IO ()
main = do
putStrLn ”Hello World”
当只有一个操作,因此没有什么要绑定时,do关键字是多余的,但为了保持一致性,我还是保留了它。
()类型表示“无效”。这种特殊的返回类型仅适用于因其副作用而调用的IO函数。
更长的示例:
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn "hello" ++ name
这构建了一个IO操作链,因为它们被分配给主功能,所以它们被执行。
将IO与Maybe进行比较表明了monad模式的多功能性。对于Maybe,该模式用于通过将条件逻辑移动到绑定函数来避免重复代码。对于IO,该模式用于确保IO类型的所有操作都是有序的,并且IO操作不会“泄漏”到纯函数。
总结
在我的主观看法中,monad模式只有在对该模式有一些内置支持的语言中才真正有价值。否则,它只会导致过于复杂的代码。但是Haskell(和其他一些语言)有一些内置支持,隐藏了繁琐的部分,然后该模式可以用于各种有用的事情。喜欢:
避免重复代码(可能)为程序的分隔区域添加可变状态或异常等语言特性。将讨厌的东西与美好的东西隔离开来(IO)嵌入式域特定语言(解析器)将GOTO添加到语言中。
这个答案从一个激励性的例子开始,通过这个例子,得出一个单子的例子,并正式定义了“单子”。
考虑伪代码中的这三个函数:
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。
monad是用于封装状态变化的对象的东西。在不允许您具有可修改状态的语言(例如,Haskell)中最常遇到这种情况。
例如文件I/O。
您将能够使用文件I/O的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是一种函数组合技术,它使用组合函数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。