在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
当前回答
monad是用于封装状态变化的对象的东西。在不允许您具有可修改状态的语言(例如,Haskell)中最常遇到这种情况。
例如文件I/O。
您将能够使用文件I/O的monad来将不断变化的状态本质与使用monad的代码隔离开来。Monad内部的代码可以有效地忽略Monad外部世界的变化状态,这使您更容易理解程序的整体效果。
其他回答
另一种尝试是解释monad,只使用Python列表和map函数。我完全接受这不是一个完整的解释,但我希望它能触及核心概念。
我从Monads上的funfunfunction视频和Learn You A Haskell章节“为了几个Monads更多”中得到了这一点的基础。我强烈推荐观看funfunfunction视频。
最简单的是,Monad是具有map和flatMap函数(在Haskell中绑定)的对象。有一些额外的必需财产,但这些是核心属性。
flatMap“展平”map的输出,对于列表,这只是连接列表的值,例如。
concat([[1], [4], [9]]) = [1, 4, 9]
因此,在Python中,我们基本上可以通过以下两个函数实现Monad:
def flatMap(func, lst):
return concat(map(func, lst))
def concat(lst):
return sum(lst, [])
func是任何接受值并返回列表的函数。
lambda x: [x*x]
解释
为了清楚起见,我通过一个简单的函数在Python中创建了concat函数,该函数将列表相加,即[]+[1]+[4]+[9]=[1,4,9](Haskell有一个原生的concat方法)。
我假设你知道地图功能是什么,例如:
>>> list(map(lambda x: [x*x], [1,2,3]))
[[1], [4], [9]]
展平是Monad的关键概念,对于每个作为Monad的对象,这种展平允许您获得Monad中包裹的值。
现在我们可以呼叫:
>>> flatMap(lambda x: [x*x], [1,2,3])
[1, 4, 9]
这个lambda取一个值x并将其放入一个列表中。monad适用于从值到monad类型的任何函数,所以在本例中是列表。
这是你的monad定义。
我认为为什么它们有用的问题已经在其他问题中得到了回答。
更多说明
其他不是列表的例子有JavaScript Promise,它有then方法,JavaScript Streams有flatMap方法。
因此Promise和Streams使用了一个稍微不同的函数,它将Stream或Promise展平,并从内部返回值。
Haskell列表monad具有以下定义:
instance Monad [] where
return x = [x]
xs >>= f = concat (map f xs)
fail _ = []
即有三个函数return(不要与大多数其他语言中的return混淆)、>>=(flatMap)和fail。
希望您能看到以下两者之间的相似之处:
xs >>= f = concat (map f xs)
and:
def flatMap(f, xs):
return concat(map(f, xs))
世界需要的是另一篇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?
看到其他答案,似乎可以很自由地跳到细节中。
数学思维
简而言之:用于组合计算的代数结构。
返回数据:创建一个只需在monad世界中生成数据的计算。
(return data)>>=(return func):第二个参数接受第一个参数作为数据生成器,并创建连接它们的新计算。
您可以认为(>>=)和return本身不会进行任何计算。他们只是简单地组合和创建计算。
当且仅当main触发时,任何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空间中的正方形,它的面积绝对不能是任何东西,只有它的长度平方,这几乎是微不足道的。这是非常强大的,因为我们不需要断言我们的世界是这样的,我们只需要使用现实的含义来预测它。”防止我们的节目偏离轨道。
我几乎可以肯定是错的,但我认为这可以帮助一些人,所以希望它能帮助一些人。