就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?

它解决了什么问题,最常用的地方是什么?

使现代化

为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?


当前回答

从实践的角度来看(总结了之前许多回答和相关文章中所说的内容),在我看来,monad的一个基本“目的”(或有用性)是利用递归方法调用(即函数组合)中隐含的依赖关系(即,当f1调用f2调用f3时,f3需要在f1之前的f2之前求值),以自然的方式表示顺序组合,特别是在惰性评估模型的上下文中(即,作为一个普通序列的顺序合成,例如C中的“f3();f2();f1();”),如果你想到f3、f2和f1实际上什么都不返回的情况(它们作为f1(f2(f3))的链接是人为的,纯粹是为了创建序列),那么这个技巧就特别明显了。

当涉及到副作用时,这一点尤其重要,即当某些状态被改变时(如果f1、f2、f3没有副作用,那么它们的求值顺序无关紧要;这是纯函数语言的一个很好的特性,例如能够并行化这些计算)。函数越纯越好。

我认为,从这个狭隘的角度来看,monad可以被视为支持惰性求值的语言的语法糖(只有在绝对必要时才求值,遵循不依赖于代码表示的顺序),并且没有其他表示顺序合成的方法。最终的结果是,“不纯”(即确实有副作用)的代码段可以以命令式的方式自然呈现,但与纯函数(没有副作用)完全分离,纯函数可以延迟求值。

正如这里所警告的,这只是一个方面。

其他回答

我能想到的最简单的解释是,单声道是一种用符号化结果组成函数的方式(也称为克莱斯利合成)。“embelished”函数具有签名a->(b,smth),其中a和b是可能彼此不同但不一定不同的类型(想想Int,Bool),smth是“上下文”或“embelisement”。

这种类型的函数也可以写成a->m b,其中m相当于“embelisation”smth。因此,这些是在上下文中返回值的函数(想想记录其操作的函数,其中smth是日志消息;或者执行输入/输出的函数,其结果取决于IO操作的结果)。

monad是一个接口(“typeclass”),它让实现者告诉它如何组合这样的函数。实现者需要为任何想要实现接口的m类型定义一个组合函数(a->mb)->(b->mc)->(a->mc)(这是Kleisli组合)。

所以,如果我们说我们有一个元组类型(Int,String),它表示Int上的计算结果,(_,String)是“embelisation”-动作的日志-和两个函数increment::Int->(Int,String)和twoTimes::Int->(Int、String),我们希望获得一个函数incamentThenDouble::Int->(Int),这是两个函数的组合,也考虑了日志。

在给定的示例中,两个函数的monad实现应用于整数值2增量ThenDouble 2(等于2倍(增量2))将返回(6,“加法1”。中间结果的增量2等于(3,“加1”),2乘以3等于(6,“加3”)

从这个Kleisli合成函数可以导出通常的一元函数。

我分享了我对蒙纳斯的理解,这在理论上可能并不完美。Monad是关于上下文传播的。Monad就是,您为某些数据(或数据类型)定义一些上下文,然后定义该上下文将如何在整个处理管道中与数据一起传递。定义上下文传播主要是定义如何合并多个上下文(相同类型)。使用Monads还意味着确保这些上下文不会意外地从数据中剥离。另一方面,可以将其他无上下文数据带入新的或现有的上下文中。然后,可以使用这个简单的概念来确保程序的编译时正确性。

为了尊重快速阅读者,我首先从精确的定义开始,继续快速的“简单的英语”解释,然后转到示例。

这里有一个简洁而精确的定义,稍微改写了一下:

monad(在计算机科学中)是一个正式的地图,它:将某些给定编程语言的每个类型X发送到一个新的类型T(X)(称为“值为X的T计算类型”);配备有一个规则,用于组合表单的两个功能f: X->T(Y)和g:Y->T(Z)到函数g∘f:X->T;以一种在明显意义上是关联的并且与称为pure_X:X->T(X)的给定单位函数是单位的方式,可以被认为是将一个值带到纯计算,而纯计算只是返回该值。

因此,简单地说,monad是从任何类型X传递到另一类型T(X)的规则,也是从两个函数f:X->T(Y)和g:Y->T(Z)(您想合成但不能合成)传递到新函数h:X->T(Z)的规则。然而,这并不是严格数学意义上的作文。我们基本上是在“弯曲”函数的组成或重新定义函数的组成方式。

此外,我们需要monad的合成规则来满足“显而易见”的数学公理:

联想性:先用g合成f,然后用h(从外部)合成,应该与先用h合成g,然后用f(从内部)合成相同。酉性质:用任意一侧的单位函数合成f应得到f。

同样,简单地说,我们不能随心所欲地重新定义函数组合:

我们首先需要关联性,以便能够在一行中组合多个函数,例如f(g(h(k(x))),而不用担心指定组合函数对的顺序。由于monad规则只规定了如何组成一对函数,如果没有这个公理,我们需要知道哪个函数对是首先组成的,依此类推。第二,我们需要单位性质,也就是简单地说,恒等式以我们期望的方式构成。因此,只要可以提取这些标识,我们就可以安全地重构函数。

简单地说:monad是类型扩展和组合函数的规则,满足两个公理——结合性和单位性质。

实际上,您希望monad由负责编写函数的语言、编译器或框架为您实现。因此,您可以专注于编写函数的逻辑,而不用担心它们的执行是如何实现的。

简而言之,这就是它的本质。


作为一名职业数学家,我倾向于避免将h称为f和g的“组合”,因为在数学上,它不是。将其称为“组成”错误地假定h是真正的数学组成,但它不是。它甚至不是由f和g唯一决定的,而是我们的monad新的“组成函数的规则”的结果。这可能与实际的数学组成完全不同,即使后者存在!


为了使它不那么干燥,让我试着用例子来说明我正在用小部分进行注释,因此您可以直接跳到要点。

作为Monad示例引发异常

假设我们要组成两个函数:

f: x -> 1 / x
g: y -> 2 * y

但未定义f(0),因此引发异常e。那么如何定义组成值g(f(0))?当然,再次抛出异常!可能是相同的e.可能是新更新的异常e1。

这里到底发生了什么?首先,我们需要新的异常值(不同或相同)。你可以称它们为nothing或null或其他任何东西,但本质保持不变——它们应该是新的值,例如,在我们这里的示例中,它不应该是数字。我宁愿不将它们称为null,以避免混淆null如何在任何特定语言中实现。同样,我更倾向于避免任何事情,因为它经常与null联系在一起,原则上,这是null应该做的,然而,无论出于什么实际原因,这个原则经常会被扭曲。

例外到底是什么?

这对任何有经验的程序员来说都是一件小事,但我想说几句话来消除任何困惑:

异常是一个封装有关执行的无效结果如何发生的信息的对象。

这可以包括丢弃任何细节并返回单个全局值(如NaN或null),或者生成一个长日志列表或发生了什么,将其发送到数据库并在分布式数据存储层上进行复制;)

这两种极端例外情况之间的重要区别在于,在第一种情况下,没有副作用。第二个是。这就引出了(千美元)问题:

纯函数中是否允许异常?

简短的回答:是的,但前提是它们不会导致副作用。

更长的答案。为了保持纯粹,函数的输出必须由其输入唯一确定。因此,我们通过将0发送到我们称为异常的新抽象值e来修改函数f。我们确保值e不包含外部信息,这些信息不是由我们的输入(即x)唯一确定的。因此,这里是一个没有副作用的异常示例:

e = {
  type: error, 
  message: 'I got error trying to divide 1 by 0'
}

这里有一个副作用:

e = {
  type: error, 
  message: 'Our committee to decide what is 1/0 is currently away'
}

事实上,只有当这种信息在未来可能改变时,它才会产生副作用。但如果它被保证永远不会改变,那么这个值就会变得唯一可预测,因此不会有副作用。

让它更傻。返回42的函数显然是纯的。但如果有人疯狂地决定将42作为一个变量,这个值可能会改变,那么在新的条件下,同样的函数就不再是纯函数。

注意,为了简单起见,我使用了对象文字符号来演示其本质。不幸的是,在JavaScript这样的语言中,错误并不是一种我们想要的函数组合方式,而像null或NaN这样的实际类型并不是这种方式,而是经过一些人工的、不总是直观的类型转换。

类型扩展名

当我们想要改变异常内部的消息时,我们实际上是在为整个异常对象声明一个新的类型E,然后这就是may数的作用,除了它令人困惑的名称,它要么是类型number,要么是新的异常类型E,所以它实际上是number和E的并数|E。特别是,它取决于我们想要如何构造E,这既不是建议的,也不是反映在名称may数中。

什么是功能成分?

这是取函数的数学运算f: X->Y和g:Y->Z和构造它们的组成作为满足h(X)=g(f(X))的函数h:X->Z。当结果f(x)不允许作为g的参数时,就会出现这个定义的问题。

在数学中,没有额外的工作,这些函数是无法合成的。对于我们上面的f和g的例子,严格的数学解决方案是从f的定义集合中删除0。有了新的定义集合(新的更严格的x类型),f变得可以与g组合。

然而,在编程中这样限制f的定义集是不太实际的。相反,可以使用异常。

或者作为另一种方法,人工值被创建为NaN、undefined、null、Infinity等。因此,您可以计算1/0到Infinity和1/-0到Infinity。然后将新值强制返回到表达式中,而不是引发异常。导致您可能会或可能无法预测的结果:

1/0                // => Infinity
parseInt(Infinity) // => NaN
NaN < 0            // => false
false + 1          // => 1

我们又回到了常规数字,准备继续前进;)

JavaScript允许我们以任何代价继续执行数值表达式,而不会像上面的示例那样抛出错误。这意味着,它还允许组合函数。这正是monad的意义所在——这是一条规则,可以组成满足本答案开头定义的公理的函数。

但是,由JavaScript处理数字错误的实现所产生的组成函数的规则是一个monad吗?

要回答这个问题,你只需要检查公理(这里作为练习而不是问题的一部分;)。

抛出异常可以用于构造monad吗?

事实上,一个更有用的monad应该是规定如果f对某个x抛出异常,那么它与任何g的组合也是如此。Plus使异常E全局唯一,只有一个可能的值(范畴理论中的终端对象)。现在这两个公理可以立即检查,我们得到了一个非常有用的monad。其结果就是众所周知的可能是莫纳德。

快速解释:

单体(在函数式编程中)是具有上下文相关行为的函数。

上下文作为参数传递,从先前的monad调用返回。它使它看起来像是同一个参数在后续调用中产生了不同的返回值。

等效值:Monad是其实际参数取决于调用链的过去调用的函数。

典型示例:有状态函数。

FAQ

等等,你说的“行为”是什么意思?

行为是指特定输入的返回值和副作用。

但它们有什么特别之处?

在过程语义中:没有。但它们仅使用纯函数进行建模。这是因为像Haskell这样的纯函数编程语言只使用本身没有状态的纯函数。

但是,国家从何而来?

状态性来自函数调用执行的顺序性。它允许嵌套函数通过多个函数调用拖动某些参数。这将模拟状态。monad只是一种软件模式,它将这些附加参数隐藏在光鲜亮丽的函数的返回值后面,通常称为return和bind。

为什么在Haskell中输入/输出是monad?

因为显示的文本是操作系统中的一种状态。如果多次读取或写入同一文本,则每次调用后操作系统的状态将不相同。相反,输出设备将显示文本输出的3倍。为了对操作系统做出正确的反应,Haskell需要将操作系统状态建模为monad。

从技术上讲,你不需要monad的定义。纯粹的函数式语言可以将“唯一性类型”的概念用于相同的目的。

单子在非功能语言中存在吗?

是的,基本上,解释器是一个复杂的monad,解释每个指令并将其映射到操作系统中的一个新状态。

详细说明:

monad(在函数式编程中)是一种纯函数式软件模式。monad是一个自动维护的环境(一个对象),可以在其中执行一系列纯函数调用。函数结果修改或与该环境交互。

换句话说,monad是一个“函数中继器”或“函数链接器”,它在自动维护的环境中链接和评估参数值。链接的参数值通常是“更新函数”,但实际上可以是任何对象(具有组成容器的方法或容器元素)。monad是在每个求值参数前后执行的“粘合代码”。这个粘合代码函数“bind”应该将每个参数的环境输出集成到原始环境中。

因此,monad以特定于特定monad的实现方式连接所有参数的结果。控制和数据是否或如何在参数之间流动也是特定于实现的。

这种交织执行允许模拟完整的命令式控制流(如GOTO程序中的)或并行执行,仅使用纯函数,还可以在函数调用之间进行副作用、临时状态或异常处理,即使应用的函数不知道外部环境。

编辑:请注意,monads可以以任何类型的控制流图来评估功能链,甚至是非确定性NFA式的方式,因为剩余的链是延迟评估的,可以在链的每个点进行多次评估,这允许在链中进行回溯。

使用monad概念的原因是纯函数范式,它需要一个工具来以纯方式模拟典型的无可指责的建模行为,而不是因为它们做了一些特殊的事情。

面向OOP人群的修道院

在OOP中,monad是一个典型的对象

通常称为return的构造函数,它将值转换为环境的初始实例一种可链接的参数应用程序方法,通常称为bind,它使用作为参数传递的函数的返回环境来维护对象的状态。

有些人还提到了第三个函数join,它是bind的一部分。因为“参数函数”在环境中求值,所以它们的结果嵌套在环境本身中。join是“取消嵌套”结果(使环境变平)的最后一步,用新环境替换环境。

monad可以实现Builder模式,但允许更广泛的使用。

示例(Python)

我认为monad最直观的例子是Python中的关系运算符:

result =  0 <= x == y < 3

您可以看到它是一个monad,因为它必须携带一些布尔状态,而这些状态是单个关系运算符调用所不知道的。

如果您考虑如何在低级别上实现它而不发生短路行为,那么您将得到一个monad实现:

# result = ret(0)
result = (0, true)
# result = result.bind(lambda v: (x, v <= x))
result[1] = result[1] and result[0] <= x
result[0] = x
# result = result.bind(lambda v: (y, v == y))
result[1] = result[1] and result[0] == y
result[0] = y
# result = result.bind(lambda v: (3, v < 3))
result[1] = result[1] and result[0] < 3
result[0] = 3
result = result[1]      # not explicit part of a monad

真正的monad最多只能计算一次每个参数。

现在考虑一下“result”变量,就会得到这个链:

ret(0) .bind (lambda v: v <= x) .bind (lambda v: v == y) .bind (lambda v: v < 3)

为什么我们需要单子?

我们只想使用函数编程。(毕竟是“功能编程”-FP)。然后,我们遇到了第一个大问题。这是一个程序:f(x)=2*xg(x,y)=x/y我们怎么能说首先要执行什么?我们如何使用不超过个函数来形成一个有序的函数序列(即程序)?解决方案:组合函数。如果你先要g,然后要f,只需写f(g(x,y))。好的,但是。。。更多问题:某些函数可能会失败(即g(2,0),除以0)。我们在FP中没有“例外”。我们如何解决它?解决方案:让我们允许函数返回两种东西:而不是g:Real,Real->Real(函数从两个实数转换为实数),让我们允许g:Real、Real->Real|Nothing(函数从一个实数转换成(实数或零))。但函数应该(更简单地)只返回一件事。解决方案:让我们创建一种要返回的新类型的数据,一种“装箱类型”,它可能包含一个真实的数据,也可能只是一个空数据。因此,我们可以有g:真实,真实->可能真实。好的,但是。。。f(g(x,y))现在发生了什么?f还没有准备好使用“也许真的”。而且,我们不想改变我们可以与g连接的每一个函数,以使用Maybe Real。解决方案:让我们有一个特殊的函数来“连接”/“组合”/“链接”函数。这样,我们就可以在幕后调整一个函数的输出,以支持下一个函数。在我们的例子中:g>>=f(连接/合成g到f)。我们希望>>=获取g的输出,检查它,如果它是Nothing,则不要调用f并返回Nothing;或者相反,提取装箱的实数并用它来馈送f。(此算法只是Maye类型的>>=的实现)。出现了许多其他问题,可以使用相同的模式来解决:1。使用“框”来编码/存储不同的含义/值,并具有像g这样的函数来返回这些“框值”。2.让作曲家/链接器g>>=f帮助将g的输出连接到f的输入,这样我们就不必改变f。使用该技术可以解决的显著问题有:具有函数序列中的每个函数(“程序”)可以共享的全局状态:解StateMonad。我们不喜欢“不纯函数”:对相同输入产生不同输出的函数。因此,让我们标记这些函数,使它们返回一个标记/装箱的值:IOmonad。

完全幸福!!!!