就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
当前回答
快速解释:
单体(在函数式编程中)是具有上下文相关行为的函数。
上下文作为参数传递,从先前的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)
其他回答
我想说,与monads最接近的OO类比是“命令模式”。
在命令模式中,将普通语句或表达式包装在命令对象中。命令对象公开执行包装语句的执行方法。所以,语句被转换为可以随意传递和执行的第一类对象。可以组合命令,以便通过链接和嵌套命令对象来创建程序对象。
命令由单独的对象调用程序执行。使用命令模式(而不仅仅是执行一系列普通语句)的好处是,不同的调用程序可以将不同的逻辑应用于如何执行命令。
命令模式可用于添加(或删除)宿主语言不支持的语言功能。例如,在没有异常的假设OO语言中,可以通过向命令公开“try”和“throw”方法来添加异常语义。当命令调用throw时,调用程序会回溯到命令列表(或树),直到最后一次“try”调用。相反,您可以通过捕获每个单独命令抛出的所有异常,并将它们转换为错误代码,然后传递给下一个命令,从而从语言中删除异常语义(如果您认为异常是坏的)。
甚至更花哨的执行语义(如事务、非确定性执行或延续)也可以用本机不支持的语言实现。如果你仔细想想,这是一个非常强大的模式。
实际上,命令模式并没有像这样作为通用语言特性使用。将每个语句转换为单独的类的开销将导致无法忍受的样板代码。但原则上,它可以用于解决与在fp中使用monad解决的问题相同的问题。
为什么我们需要单子?
我们只想使用函数编程。(毕竟是“功能编程”-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。
完全幸福!!!!
我分享了我对蒙纳斯的理解,这在理论上可能并不完美。Monad是关于上下文传播的。Monad就是,您为某些数据(或数据类型)定义一些上下文,然后定义该上下文将如何在整个处理管道中与数据一起传递。定义上下文传播主要是定义如何合并多个上下文(相同类型)。使用Monads还意味着确保这些上下文不会意外地从数据中剥离。另一方面,可以将其他无上下文数据带入新的或现有的上下文中。然后,可以使用这个简单的概念来确保程序的编译时正确性。
monad在OO中是否具有“自然”解释取决于monad。在像Java这样的语言中,您可以将may monad转换为检查空指针的语言,这样失败的计算(即,在Haskell中生成Nothing)会将空指针作为结果发出。您可以将状态monad转换为通过创建可变变量和更改其状态的方法生成的语言。
monad是内函子范畴中的幺半群。
这句话所表达的信息非常深刻。你在一个monad中使用任何命令式语言。monad是一种“有序”的领域特定语言。它满足某些有趣的财产,这些属性使单子函数成为“命令式编程”的数学模型。Haskell使定义小型(或大型)命令式语言变得容易,这些语言可以以多种方式组合。
作为一名OO程序员,您使用语言的类层次结构来组织可以在上下文中调用的函数或过程的类型,即您所称的对象。monad也是对这个概念的抽象,因为不同的monad可以以任意方式组合,有效地将所有子monad的方法“导入”到范围中。
从体系结构上讲,然后使用类型签名来明确表示可以使用哪些上下文来计算值。
可以使用monad转换器来实现这一目的,并且有一个高质量的所有“标准”monad集合:
列表(通过将列表视为域进行非确定性计算)可能(计算可能失败,但报告不重要)错误(可能失败并需要异常处理的计算Reader(可以由普通Haskell函数组合表示的计算)编写器(使用顺序“渲染”/“记录”(到字符串、html等)进行计算)续(续)IO(取决于底层计算机系统的计算)状态(上下文包含可修改值的计算)
具有相应的monad变压器和类型类别。类型类允许通过统一monad的接口来组合monad,从而使具体monad可以实现monad“类”的标准接口。例如,模块Control.Monad.State包含一个类MonadState s m,(State s)是表单的一个实例
instance MonadState s (State s) where
put = ...
get = ...
长话短说,monad是一个函子,它将“上下文”附加到一个值上,它可以向monad中注入一个值,并且可以根据附加到它上的上下文来评估值,至少是以受限的方式。
So:
return :: a -> m a
是一个函数,它将a类型的值注入到m类型的monad“action”中。
(>>=) :: m a -> (a -> m b) -> m b
是一个执行monad操作、评估其结果并将函数应用于结果的函数。(>>=)的妙处在于结果在同一个monad中。换句话说,在m>>=f中,(>>=)从m中提取结果,并将其绑定到f,这样结果就在monad中。(或者,我们可以说(>>=)将f拉入m,并将其应用于结果。)因此,如果我们有f::a->m b和g::b->m c,我们可以“排序”动作:
m >>= f >>= g
或者,使用“do符号”
do x <- m
y <- f x
g y
(>>)的类型可能是发光的。它是
(>>) :: m a -> m b -> m b
它对应于C等过程语言中的(;)运算符。它允许使用以下表示法:
m = do x <- someQuery
someAction x
theNextAction
andSoOn
在数学和哲学逻辑中,我们有框架和模型,这些框架和模型“自然”地用单子主义建模。解释是一种函数,它查看模型的域,并计算命题(或公式,在推广下)的真值(或推广)。在必要性的模态逻辑中,我们可能会说,如果命题在“每个可能的世界”中都是真的,那么它是必要的——如果它在每个可容许的域中都是真实的。这意味着命题语言中的模型可以具体化为一个模型,其域由不同模型的集合组成(一个对应于每个可能的世界)。每个monad都有一个名为“join”的方法,该方法将分层,这意味着结果为monad动作的每个monad动作都可以嵌入到monad中。
join :: m (m a) -> m a
更重要的是,这意味着monad在“层堆叠”操作下关闭。这就是monad转换器的工作原理:它们通过为以下类型提供“类联接”方法来组合monad:
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
这样我们就可以将(MaybeT m)中的作用转换为m中的作用,有效地折叠层。在本例中,runMaybeT::MaybeT m a->m(Maybe a)是我们的类联接方法。(MaybeT m)是一个monad,MaybeT::m(Maybe a)->MaybeT ma实际上是m中一个新类型monad动作的构造函数。
函子的自由单元是通过堆叠f生成的单元,这意味着f的每个构造函数序列都是自由单元的元素(或者更确切地说,是与f的构造函数序列树形状相同的元素)。自由单体是一种用最少的锅炉板构建柔性单体的有用技术。在Haskell程序中,我可能会使用自由monad来为“高级系统编程”定义简单monad,以帮助维护类型安全(我只是在使用类型及其声明。使用组合子可以直接实现):
data RandomF r a = GetRandom (r -> a) deriving Functor
type Random r a = Free (RandomF r) a
type RandomT m a = Random (m a) (m a) -- model randomness in a monad by computing random monad elements.
getRandom :: Random r r
runRandomIO :: Random r a -> IO a (use some kind of IO-based backend to run)
runRandomIO' :: Random r a -> IO a (use some other kind of IO-based backend)
runRandomList :: Random r a -> [a] (some kind of list-based backend (for pseudo-randoms))
Monastem是您可能称之为“解释器”或“命令”模式的基础架构,抽象为最清晰的形式,因为每个单元计算都必须“运行”,至少是微不足道的。(运行时系统为我们运行IO monad,是任何Haskell程序的入口点。IO通过按顺序运行IO操作来“驱动”其余的计算)。
join的类型也是我们得到monad是内函子范畴中的幺半群的陈述的地方。由于其类型,联接对于理论目的来说通常更为重要。但了解类型意味着了解单子。Join和monad变换器的类Join类型在函数组合的意义上是内函子的有效组合。把它放在类似Haskell的伪语言中,
Foo::m(m a)<->(m.m)a
我将尝试使用OOP术语做出最简短的定义:
如果一个泛型类CMonadic<T>至少定义了以下方法,那么它就是一个monad:
class CMonadic<T> {
static CMonadic<T> create(T t); // a.k.a., "return" in Haskell
public CMonadic<U> flatMap<U>(Func<T, CMonadic<U>> f); // a.k.a. "bind" in Haskell
}
如果以下定律适用于所有类型T及其可能的值T
左标识:
CMonadic<T>.create(t).flatMap(f) == f(t)
权利认同
instance.flatMap(CMonadic<T>.create) == instance
关联性:
instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))
示例:
列表monad可能具有:
List<int>.create(1) --> [1]
列表[1,2,3]上的flatMap可以这样工作:
intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]
Iterables和Observables也可以是monadic,以及Promise和Task。
评论:
修道院没有那么复杂。flatMap函数与常见的map非常相似。它接收一个函数参数(也称为委托),可以使用来自泛型类的值调用(立即或稍后,零次或多次)。它希望传递的函数也将其返回值包装在同一类泛型类中。为了帮助实现这一点,它提供了create,一个构造函数,可以从值创建该泛型类的实例。flatMap的返回结果也是相同类型的泛型类,通常将flatMap一个或多个应用程序的返回结果中包含的相同值打包到先前包含的值。这允许您尽可能多地链接flatMap:
intList.flatMap(x => List<int>.makeFromTwo(x, x*10))
.flatMap(x => x % 3 == 0
? List<string>.create("x = " + x.toString())
: List<string>.empty())
恰好这种泛型类作为大量事物的基础模型非常有用。这(加上范畴理论的对立)是莫纳斯看起来如此难以理解或解释的原因。它们是一个非常抽象的东西,只有在它们被专门化之后才会变得明显有用。
例如,可以使用一元容器对异常进行建模。每个容器将包含操作结果或发生的错误。flatMap回调链中的下一个函数(委托)只有在前一个函数将值打包到容器中时才会被调用。否则,如果打包了错误,错误将继续在链接的容器中传播,直到找到通过名为.orElse()的方法附加了错误处理程序函数的容器(这样的方法将是允许的扩展)
注意:函数式语言允许您编写可以对任何类型的一元泛型类进行操作的函数。要实现这一点,必须为monad编写一个通用接口。我不知道是否有可能用C#编写这样的接口,但据我所知,这不是:
interface IMonad<T> {
static IMonad<T> create(T t); // not allowed
public IMonad<U> flatMap<U>(Func<T, IMonad<U>> f); // not specific enough,
// because the function must return the same kind of monad, not just any monad
}