在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?

我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。


当前回答

解释“什么是monad”有点像说“什么是数字?”我们总是使用数字。但想象一下,你遇到了一个对数字一无所知的人。你怎么解释数字是什么?你怎么开始描述为什么这可能有用?

什么是单子?简单的回答是:这是一种将操作链接在一起的特定方式。

本质上,您正在编写执行步骤,并将它们与“绑定函数”链接在一起。(在Haskell中,它名为>>=。)您可以自己编写对绑定运算符的调用,也可以使用语法糖,使编译器为您插入这些函数调用。但无论哪种方式,每个步骤都由对该绑定函数的调用分隔。

因此绑定函数就像分号;它将流程中的步骤分开。bind函数的任务是获取上一步的输出,并将其输入下一步。

听起来不太难,对吧?但单子不止一种。为什么?怎样

好吧,bind函数可以从一个步骤中获取结果,并将其传递给下一个步骤。但如果这就是单子的全部。。。这实际上不是很有用。理解这一点很重要:每个有用的monad除了做monad之外,还做其他事情。每一个有用的单子都有一种“特殊的力量”,这使它独一无二。

(没有什么特别作用的monad被称为“身份monad”。与身份函数类似,这听起来是一件毫无意义的事情,但事实证明并非如此……但这是另一回事™.)

基本上,每个monad都有自己的绑定函数实现。你可以编写一个绑定函数,这样它就可以在执行步骤之间做一些傻事。例如:

如果每个步骤都返回一个成功/失败指示符,则只有在前一个步骤成功的情况下,才能让绑定执行下一个步骤。这样,失败的步骤“自动”中止整个序列,而无需您进行任何条件测试。(故障单)扩展这个想法,您可以实现“异常”。(错误单点或异常单点。)因为您自己定义它们,而不是将其作为一种语言特性,所以您可以定义它们的工作方式。(例如,您可能希望忽略前两个异常,仅在引发第三个异常时中止。)您可以使每个步骤返回多个结果,并让bind函数对其进行循环,将每个结果输入到下一步。这样,在处理多个结果时,就不必一直到处写循环。绑定函数“自动”为您完成所有这些。(单子)除了将“结果”从一个步骤传递到另一个步骤之外,还可以让bind函数传递额外的数据。这些数据现在不会显示在源代码中,但您仍然可以从任何地方访问它,而无需手动将其传递给每个函数。(《读者》杂志)您可以这样做,以便可以替换“额外数据”。这允许您模拟破坏性更新,而无需实际执行破坏性更新。(莫纳德州及其堂弟作家莫纳德。)因为您只是在模拟破坏性更新,所以您可以轻松地完成真正的破坏性更新所无法完成的事情。例如,您可以撤消上一次更新,或恢复到旧版本。你可以制作一个可以暂停计算的monad,这样你就可以暂停你的程序,进入并修补内部状态数据,然后恢复它。您可以将“continuations”实现为monad。这可以让你打破人们的想法!

所有这些和更多的都可以通过monad实现。当然,这一切在没有单子的情况下也是完全可能的。使用monad非常简单。

其他回答

如果你要求对如此抽象的东西做出简洁、实用的解释,那么你只能希望得到一个抽象的答案:

a -> b

是表示从as到bs的计算的一种方式。您可以将计算链接起来,也可以将它们组合在一起:

(b -> c) -> (a -> b) -> (a -> c)

更复杂的计算需要更复杂的类型,例如:

a -> f b

是从as到bs到fs的计算类型。您还可以编写它们:

(b -> f c) -> (a -> f b) -> (a -> f c)

事实证明,这种模式无处不在,并且与上面的第一个组合具有相同的财产(结合性、右-和左-同一性)。

人们必须给这个模式起一个名字,但如果知道第一个组合被正式描述为半群体,这会有帮助吗?

“单子和圆括号一样有趣和重要”(奥列格·基斯廖夫)

[免责声明:我仍在努力完全了解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),而是实际上让代码更简单。

对于来自命令式背景(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博客文章,但我认为这对识别野外现存的monad很有用。

单子是分形

上面是一个叫做Sierpinski三角形的分形,这是我唯一记得画的分形。分形是与上述三角形相似的自相似结构,其中部分与整体相似(在这种情况下,正好是母三角形比例的一半)。单子是分形。给定一个一元数据结构,它的值可以组合成数据结构的另一个值。这就是为什么它对编程有用,这也是为什么它在许多情况下都会出现。

monad是一个容器,但用于数据。一个特殊的容器。

所有容器都可以有开口、把手和喷口,但这些容器都保证有一定的开口、把手或喷口。

为什么?因为这些有保证的开口、把手和喷口对于以特定、常见的方式拾取和连接容器非常有用。

这使您可以选择不同的容器,而不必对它们了解太多。它还允许不同类型的容器轻松连接在一起。