在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
当前回答
monad实际上是“类型运算符”的一种形式。它将做三件事。首先,它会将一种类型的值“包装”(或以其他方式转换)为另一种类型(通常称为“一元类型”)。第二,它将使底层类型上的所有操作(或函数)在monadic类型上可用。最后,它将为将自身与另一个monad组合以生成复合monad提供支持。
“可能monad”本质上等同于Visual Basic/C#中的“可为null的类型”。它接受不可为null的类型“T”并将其转换为“可为null<T>”,然后定义所有二进制运算符在可为null><T>上的含义。
副作用也有类似的表现。创建了一个结构,该结构包含函数返回值旁边的副作用描述。当值在函数之间传递时,“提升”操作会复制副作用。
它们被称为“monad”,而不是更容易理解的“类型运算符”的名称,原因如下:
Monad对他们的行为有限制(详见定义)。这些限制,加上涉及三个运算,符合范畴理论中一个叫做monad的结构,这是一个模糊的数学分支。它们是由“纯”函数语言的支持者设计的纯函数语言的支持者,如模糊的数学分支由于数学晦涩难懂,而且monad与特定的编程风格相关,人们倾向于使用monad这个词作为一种秘密握手。正因为如此,没有人费心去投资一个更好的名字。
其他回答
最近我一直在以不同的方式思考莫纳斯。我一直认为它们以数学的方式抽象出执行顺序,这使得新类型的多态性成为可能。
如果您使用的是命令式语言,并且您按照顺序编写一些表达式,那么代码始终按照该顺序运行。
在简单的例子中,当你使用monad时,感觉是一样的——你定义了一个按顺序发生的表达式列表。除此之外,根据您使用的monad,您的代码可能会按顺序运行(如IO monad),同时在多个项目上并行运行(如List monad);它可能会中途停止(如Maybe monad)。它可能会在中途暂停以稍后恢复(如Resume monad)),或者它可能会中途倒带以尝试其他选项(如逻辑单声道)。
因为monad是多态的,所以可以根据需要在不同的monad中运行相同的代码。
此外,在某些情况下,可以将monad组合在一起(使用monad转换器)以同时获得多个特性。
实际上,monad是函数组合运算符的一种自定义实现,它考虑了副作用以及不兼容的输入和返回值(用于链接)。
你应该首先了解函子是什么。在此之前,先了解高阶函数。
高阶函数只是一个以函数为自变量的函数。
函子是任何类型构造T,其中存在一个高阶函数,称之为map,它将类型为A->b的函数(给定任意两个类型A和b)转换为函数Ta->Tb。该map函数还必须遵守恒等式和复合法则,以便以下表达式对所有p和q返回true(Haskell表示法):
map id = id
map (p . q) = map p . map q
例如,名为List的类型构造函数是一个函子,如果它配备了一个类型为(a->b)->Lista->Listb的函数,该函数遵守上述定律。唯一实际的实施是显而易见的。生成的Lista->Listb函数在给定列表上迭代,为每个元素调用(a->b)函数,并返回结果列表。
monad本质上只是一个函子T,它有两个额外的方法,类型为T(T A)->T A的join和类型为A->T A的unit(有时称为return、fork或pure)。对于Haskell中的列表:
join :: [[a]] -> [a]
pure :: a -> [a]
为什么有用?因为例如,您可以使用返回列表的函数映射列表。Join获取生成的列表列表并将它们连接起来。列表是monad,因为这是可能的。
您可以编写一个函数,先映射,然后连接。此函数称为bind或flatMap,或(>>=)或(=<<)。这通常是Haskell中给出monad实例的方式。
monad必须满足某些定律,即联接必须是关联的。这意味着,如果您的值x类型为[[a]]],那么join(join x)应该等于join(map joinx)。纯必须是联接的标识,这样联接(纯x)==x。
在几年前回答了这个问题之后,我相信我可以通过。。。
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。
但是,你本可以发明蒙纳斯!
sigfpe说:但所有这些都将单子介绍为需要解释的深奥的东西。但我想说的是,它们一点都不深奥。事实上,面对函数式编程中的各种问题,你会不可避免地被引向某些解决方案,所有这些都是单子的例子。事实上,如果你还没有发明,我希望你现在就发明它们。这是注意到所有这些解决方案实际上都是变相的相同解决方案的一小步。读完这篇文章后,你可能会更好地理解单子上的其他文档,因为你会发现你所看到的一切都是你已经发明的。monads试图解决的许多问题都与副作用有关。因此,我们将从它们开始。(请注意,monad让您做的不仅仅是处理副作用,特别是许多类型的容器对象都可以被视为monad。monad的一些介绍发现,很难协调monad的这两种不同用法,并且只关注其中一种。)在命令式编程语言(如C++)中,函数的行为与数学函数完全不同。例如,假设我们有一个C++函数,它接受一个浮点参数并返回一个浮点结果。从表面上看,它可能有点像一个将实数映射到实数的数学函数,但C++函数可以做的不仅仅是返回一个依赖于其参数的数字。它可以读取和写入全局变量的值,也可以将输出写入屏幕并接收用户的输入。然而,在纯函数语言中,函数只能读取在其参数中提供给它的内容,而它对世界产生影响的唯一方式是通过它返回的值。