就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
就OOP程序员所能理解的(没有任何功能编程背景)而言,什么是monad?
它解决了什么问题,最常用的地方是什么?
使现代化
为了澄清我所寻求的理解,假设您正在将一个具有monad的FP应用程序转换为OOP应用程序。如何将monad的职责移植到OOP应用程序?
当前回答
典型用法中的Monad在功能上等同于过程编程的异常处理机制。
在现代过程语言中,在一系列语句周围放置一个异常处理程序,其中任何语句都可能引发异常。如果任何语句引发异常,则语句序列的正常执行将停止并转移到异常处理程序。
然而,函数式编程语言在哲学上避免了异常处理特性,因为它们的“goto”性质类似。函数编程的观点是,函数不应该有“副作用”,比如中断程序流的异常。
实际上,由于I/O的原因,在现实世界中不能排除副作用。函数式编程中的monad用于处理这一问题,方法是获取一组链式函数调用(其中任何一个都可能产生意外结果),并将任何意外结果转换为封装数据,这些数据仍然可以安全地通过剩余的函数调用。
控制流被保留,但意外事件被安全地封装和处理。
其他回答
我想说,与monads最接近的OO类比是“命令模式”。
在命令模式中,将普通语句或表达式包装在命令对象中。命令对象公开执行包装语句的执行方法。所以,语句被转换为可以随意传递和执行的第一类对象。可以组合命令,以便通过链接和嵌套命令对象来创建程序对象。
命令由单独的对象调用程序执行。使用命令模式(而不仅仅是执行一系列普通语句)的好处是,不同的调用程序可以将不同的逻辑应用于如何执行命令。
命令模式可用于添加(或删除)宿主语言不支持的语言功能。例如,在没有异常的假设OO语言中,可以通过向命令公开“try”和“throw”方法来添加异常语义。当命令调用throw时,调用程序会回溯到命令列表(或树),直到最后一次“try”调用。相反,您可以通过捕获每个单独命令抛出的所有异常,并将它们转换为错误代码,然后传递给下一个命令,从而从语言中删除异常语义(如果您认为异常是坏的)。
甚至更花哨的执行语义(如事务、非确定性执行或延续)也可以用本机不支持的语言实现。如果你仔细想想,这是一个非常强大的模式。
实际上,命令模式并没有像这样作为通用语言特性使用。将每个语句转换为单独的类的开销将导致无法忍受的样板代码。但原则上,它可以用于解决与在fp中使用monad解决的问题相同的问题。
monad是一种封装值的数据类型,本质上可以对其应用两个操作:
返回x创建封装x的monad类型的值m>>=f(读作“绑定运算符”)将函数f应用于monad m中的值
这就是monad。还有一些技术问题,但基本上这两个操作定义了monad。真正的问题是,“monad做什么?”,这取决于monad-列表是monad,Maybes是monad;IO操作是monad。当我们说这些东西是monad时,这意味着它们具有返回和>>=的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。
完全幸福!!!!
你最近有一篇演讲《Monadologie——关于类型焦虑的专业帮助》(Christopher League,2010年7月12日),这篇演讲对延续和monad的话题非常有趣。这个(幻灯片)演示的视频实际上可以在vimeo上获得。Monad部分开始于37分钟左右,在这段一小时的视频中,从58张幻灯片中的第42张幻灯片开始。
它被称为“函数式编程的主要设计模式”,但示例中使用的语言是Scala,它既是面向对象的又是函数式的。您可以在Debasish Ghosh(2008年3月27日)的博客文章“Monads-在Scala中抽象计算的另一种方法”中阅读更多关于Monad的内容。
如果类型构造函数M支持以下操作,那么它就是monad:
# the return function
def unit[A] (x: A): M[A]
# called "bind" in Haskell
def flatMap[A,B] (m: M[A]) (f: A => M[B]): M[B]
# Other two can be written in term of the first two:
def map[A,B] (m: M[A]) (f: A => B): M[B] =
flatMap(m){ x => unit(f(x)) }
def andThen[A,B] (ma: M[A]) (mb: M[B]): M[B] =
flatMap(ma){ x => mb }
例如(在Scala中):
选项是monad
def unit[A] (x: A): Option[A] = Some(x) def flatMap[A,B](m:Option[A])(f:A =>Option[B]): Option[B] = m match { case None => None case Some(x) => f(x) }
列表为Monad
def unit[A] (x: A): List[A] = List(x) def flatMap[A,B](m:List[A])(f:A =>List[B]): List[B] = m match { case Nil => Nil case x::xs => f(x) ::: flatMap(xs)(f) }
Monad在Scala中非常重要,因为它是为了利用Monad结构而构建的方便语法:
对于Scala的理解:
for {
i <- 1 to 4
j <- 1 to i
k <- 1 to j
} yield i*j*k
由编译器翻译为:
(1 to 4).flatMap { i =>
(1 to i).flatMap { j =>
(1 to j).map { k =>
i*j*k }}}
关键抽象是flatMap,它通过链接绑定计算。flatMap的每次调用都返回相同的数据结构类型(但值不同),作为链中下一个命令的输入。
在上面的代码段中,flatMap将闭包(SomeType)=>List[AanotherType]作为输入,并返回List[Aanother Type]。需要注意的一点是,所有flatMap都采用相同的闭包类型作为输入,并返回与输出相同的类型。
这就是“绑定”计算线程的原因——为了理解,序列中的每一项都必须遵守相同的类型约束。
如果您执行两个操作(可能失败)并将结果传递给第三个,例如:
lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]
但如果不利用Monad,你会得到复杂的OOP代码,比如:
val user = getLoggedInUser(session)
val confirm =
if(!user.isDefined) None
else lookupVenue(name) match {
case None => None
case Some(venue) =>
val confno = reserveTable(venue, user.get)
if(confno.isDefined)
mailTo(confno.get, user.get)
confno
}
而使用Monad,您可以像所有操作一样使用实际类型(地点、用户),并隐藏选项验证内容,这都是因为for语法的平面图:
val confirm = for {
venue <- lookupVenue(name)
user <- getLoggedInUser(session)
confno <- reserveTable(venue, user)
} yield {
mailTo(confno, user)
confno
}
只有当所有三个函数都具有Some[X]时,才会执行屈服部分;任何“无”将直接返回以确认。
So:
Monad允许在函数编程中进行有序计算,这允许我们以一种很好的结构化形式(有点像DSL)对动作序列进行建模。最大的能力来自于将服务于不同目的的monad组合成应用程序中的可扩展抽象的能力。monad对动作的排序和线程化由语言编译器完成,该语言编译器通过闭包的魔力进行转换。
顺便说一句,Monad不是FP中使用的唯一计算模型:
范畴理论提出了许多计算模型。其中计算的Arrow模型莫纳德计算模型计算的应用模型
我将尝试使用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
}