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

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


当前回答

Monoid似乎可以确保在Monoid和受支持的类型上定义的所有操作始终返回Monoid内部的受支持类型。任何数字+任何数字=一个数字,没有错误。

而除法接受两个分数,并返回一个分数,该分数在haskell somewhy中将除以零定义为无穷大(恰好是分数somewhy)。。。

在任何情况下,Monads似乎只是一种确保您的操作链以可预测的方式运行的方法,而一个声称为Num->Num的函数,由另一个用x调用的Num->Num的函数组成,并不意味着发射导弹。

另一方面,如果我们有一个功能可以发射导弹,我们可以将它与其他功能组合起来,也可以发射导弹。

在Haskell中,main的类型是IO()或IO[()],这种区分很奇怪,我不会讨论它,但我认为会发生以下情况:

如果我有main,我希望它做一系列动作,我运行程序的原因是产生一个效果——通常是通过IO。因此,我可以将IO操作串联在一起,以便——做IO,而不是其他。

如果我尝试做一些不“返回IO”的事情,程序会抱怨链不流动,或者基本上“这与我们正在尝试做的事情有什么关系——IO动作”,这似乎迫使程序员保持思路,不偏离并思考发射导弹,同时创建排序算法——不流动。

基本上,Monads似乎是编译器的一个提示,“嘿,你知道这个函数在这里返回一个数字,它实际上并不总是有效的,它有时会产生一个number,有时什么都没有,请记住这一点”。知道了这一点,如果你试图断言一个单元动作,单元动作可能会作为一个编译时异常,说“嘿,这实际上不是一个数字,这可能是一个数字。但你不能假设这一点。做一些事情以确保流是可接受的。”这在一定程度上防止了不可预测的程序行为。

似乎monad不是关于纯粹性,也不是关于控制,而是关于维护一个类别的身份,在这个类别上,所有行为都是可预测和定义的,或者不编译。当你被要求做某事时,你不能什么都不做,如果你被要求什么都不干(可见),你也不能做。

我能想到的Monads的最大原因是——看看程序/OOP代码,你会发现你不知道程序从哪里开始,也不知道程序的结束,你看到的只是大量的跳跃和大量的数学、魔法和导弹。您将无法维护它,如果可以的话,您将花费大量的时间来思考整个程序,然后才能理解其中的任何部分,因为在这种情况下,模块化是基于代码的相互依赖的“部分”,其中代码被优化为尽可能相关,以保证效率/相互关系。单子是非常具体的,并且通过定义得到了很好的定义,并确保程序流程可以进行分析,并隔离难以分析的部分——因为它们本身就是单子。monad似乎是一个“可理解的单元,它在完全理解时是可预测的”——如果你理解“可能”monad,那么它除了“可能”之外就没有可能做任何事情,这看起来微不足道,但在大多数非monad代码中,一个简单的函数“helloworld”可以发射导弹,什么都不做,或者摧毁宇宙,甚至扭曲时间——我们不知道也不能保证它是什么样子。一个单子保证它就是什么样子。这是非常强大的。

“现实世界”中的所有事物似乎都是单子,因为它受到防止混淆的明确可观察规律的约束。这并不意味着我们必须模仿这个对象的所有操作来创建类,相反,我们可以简单地说“一个正方形就是一个正方形”,只不过是一个正方形,甚至不是矩形或圆形,和“一个正方形的面积是它现有维度的长度乘以它自身的面积。无论你有什么正方形,如果它是2D空间中的正方形,它的面积绝对不能是任何东西,只有它的长度平方,这几乎是微不足道的。这是非常强大的,因为我们不需要断言我们的世界是这样的,我们只需要使用现实的含义来预测它。”防止我们的节目偏离轨道。

我几乎可以肯定是错的,但我认为这可以帮助一些人,所以希望它能帮助一些人。

其他回答

Monoid似乎可以确保在Monoid和受支持的类型上定义的所有操作始终返回Monoid内部的受支持类型。任何数字+任何数字=一个数字,没有错误。

而除法接受两个分数,并返回一个分数,该分数在haskell somewhy中将除以零定义为无穷大(恰好是分数somewhy)。。。

在任何情况下,Monads似乎只是一种确保您的操作链以可预测的方式运行的方法,而一个声称为Num->Num的函数,由另一个用x调用的Num->Num的函数组成,并不意味着发射导弹。

另一方面,如果我们有一个功能可以发射导弹,我们可以将它与其他功能组合起来,也可以发射导弹。

在Haskell中,main的类型是IO()或IO[()],这种区分很奇怪,我不会讨论它,但我认为会发生以下情况:

如果我有main,我希望它做一系列动作,我运行程序的原因是产生一个效果——通常是通过IO。因此,我可以将IO操作串联在一起,以便——做IO,而不是其他。

如果我尝试做一些不“返回IO”的事情,程序会抱怨链不流动,或者基本上“这与我们正在尝试做的事情有什么关系——IO动作”,这似乎迫使程序员保持思路,不偏离并思考发射导弹,同时创建排序算法——不流动。

基本上,Monads似乎是编译器的一个提示,“嘿,你知道这个函数在这里返回一个数字,它实际上并不总是有效的,它有时会产生一个number,有时什么都没有,请记住这一点”。知道了这一点,如果你试图断言一个单元动作,单元动作可能会作为一个编译时异常,说“嘿,这实际上不是一个数字,这可能是一个数字。但你不能假设这一点。做一些事情以确保流是可接受的。”这在一定程度上防止了不可预测的程序行为。

似乎monad不是关于纯粹性,也不是关于控制,而是关于维护一个类别的身份,在这个类别上,所有行为都是可预测和定义的,或者不编译。当你被要求做某事时,你不能什么都不做,如果你被要求什么都不干(可见),你也不能做。

我能想到的Monads的最大原因是——看看程序/OOP代码,你会发现你不知道程序从哪里开始,也不知道程序的结束,你看到的只是大量的跳跃和大量的数学、魔法和导弹。您将无法维护它,如果可以的话,您将花费大量的时间来思考整个程序,然后才能理解其中的任何部分,因为在这种情况下,模块化是基于代码的相互依赖的“部分”,其中代码被优化为尽可能相关,以保证效率/相互关系。单子是非常具体的,并且通过定义得到了很好的定义,并确保程序流程可以进行分析,并隔离难以分析的部分——因为它们本身就是单子。monad似乎是一个“可理解的单元,它在完全理解时是可预测的”——如果你理解“可能”monad,那么它除了“可能”之外就没有可能做任何事情,这看起来微不足道,但在大多数非monad代码中,一个简单的函数“helloworld”可以发射导弹,什么都不做,或者摧毁宇宙,甚至扭曲时间——我们不知道也不能保证它是什么样子。一个单子保证它就是什么样子。这是非常强大的。

“现实世界”中的所有事物似乎都是单子,因为它受到防止混淆的明确可观察规律的约束。这并不意味着我们必须模仿这个对象的所有操作来创建类,相反,我们可以简单地说“一个正方形就是一个正方形”,只不过是一个正方形,甚至不是矩形或圆形,和“一个正方形的面积是它现有维度的长度乘以它自身的面积。无论你有什么正方形,如果它是2D空间中的正方形,它的面积绝对不能是任何东西,只有它的长度平方,这几乎是微不足道的。这是非常强大的,因为我们不需要断言我们的世界是这样的,我们只需要使用现实的含义来预测它。”防止我们的节目偏离轨道。

我几乎可以肯定是错的,但我认为这可以帮助一些人,所以希望它能帮助一些人。

monad是用于封装状态变化的对象的东西。在不允许您具有可修改状态的语言(例如,Haskell)中最常遇到这种情况。

例如文件I/O。

您将能够使用文件I/O的monad来将不断变化的状态本质与使用monad的代码隔离开来。Monad内部的代码可以有效地忽略Monad外部世界的变化状态,这使您更容易理解程序的整体效果。

(另请参见“什么是monad?”中的答案)

蒙纳斯的一个很好的动机是西格菲(丹·皮波尼)的《你本可以发明蒙纳斯!(也许你已经有了)。还有很多其他monad教程,其中许多都试图使用各种类比以“简单的术语”来解释monad:这就是monad教程谬论;避开它们。

正如MacIver博士在《告诉我们为什么你的语言很糟糕》中所说:所以,我讨厌Haskell的事情:让我们从显而易见的开始。Monad教程。不,不是单子。特别是教程。他们没完没了,夸夸其谈,亲爱的上帝,他们太乏味了。此外,我从未见过任何令人信服的证据表明它们确实有帮助。阅读类定义,编写一些代码,忘掉这个可怕的名字。

你说你懂“也许莫纳德”吗?很好,你在路上了。只要开始使用其他monad,迟早你会了解monad的一般含义。

(如果你以数学为导向,你可能想忽略几十个教程,学习定义,或遵循类别理论的讲座:)定义的主要部分是Monad M包含一个“类型构造器”,为每个现有类型“T”定义一个新类型“M T”,以及在“常规”类型和“M”类型之间来回移动的一些方式。]

同样,令人惊讶的是,对monad最好的介绍之一实际上是介绍monad的早期学术论文之一,Philip Wadler的Monad for functional programming。它实际上有一些实用的、非平凡的激励性例子,与许多人工教程不同。

世界需要的是另一篇monad博客文章,但我认为这对识别野外现存的monad很有用。

单子是分形

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

经过努力,我想我终于明白了单子。在重新阅读了我自己对绝大多数投票结果的冗长批评之后,我将给出这个解释。

要理解单子,需要回答三个问题:

你为什么需要蒙纳德?什么是单子?如何实现monad?

正如我在最初的评论中所指出的,有太多的monad解释被第3个问题所困扰,没有,也没有充分地涵盖第2个问题或第1个问题。

你为什么需要蒙纳德?

Haskell等纯函数式语言与C或Java等命令式语言的不同之处在于,纯函数式程序不一定按特定顺序执行,一步一步执行。Haskell程序更类似于一个数学函数,在该函数中,您可以以任意数量的潜在阶数求解“方程”。这带来了许多好处,其中之一是它消除了某些类型的错误的可能性,特别是那些与“状态”相关的错误。

然而,使用这种编程风格,有些问题不是很容易解决的。有些事情,比如控制台编程和文件i/o,需要按照特定的顺序进行,或者需要维护状态。处理这个问题的一种方法是创建一种表示计算状态的对象,以及一系列将状态对象作为输入并返回新修改的状态对象的函数。

因此,让我们创建一个假设的“状态”值,它表示控制台屏幕的状态。这个值是如何构造的并不重要,但假设它是一个字节长度的ascii字符数组,表示屏幕上当前可见的内容,以及一个表示用户输入的最后一行伪代码的数组。我们已经定义了一些接受控制台状态、修改它并返回新控制台状态的函数。

consolestate MyConsole = new consolestate;

因此,要进行控制台编程,但以纯函数的方式,您需要在彼此之间嵌套许多函数调用。

consolestate FinalConsole = print(input(print(myconsole, "Hello, what's your name?")),"hello, %inputbuffer%!");

以这种方式编程保持了“纯”的功能风格,同时强制对控制台的更改按特定顺序进行。但是,我们可能希望像上面的示例一样,一次只执行几个操作。以这种方式嵌套函数将开始变得笨拙。我们想要的是基本上与上面相同的代码,但编写得更像这样:

consolestate FinalConsole = myconsole:
                            print("Hello, what's your name?"):
                            input():
                            print("hello, %inputbuffer%!");

这确实是一种更方便的写法。但我们如何做到这一点呢?

什么是单子?

一旦你定义了一个类型(比如consoleestate),以及一系列专门为该类型操作而设计的函数,你就可以通过定义一个操作符(比如:(bind))将这些东西的整个包变成一个“monad”,该操作符会自动将返回值输入到左边的函数参数中,转换为与特定类型的绑定运算符一起工作的函数。

如何实现monad?

看到其他答案,似乎可以很自由地跳到细节中。