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

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

使现代化

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


当前回答

monad是一种封装值的数据类型,本质上可以对其应用两个操作:

返回x创建封装x的monad类型的值m>>=f(读作“绑定运算符”)将函数f应用于monad m中的值

这就是monad。还有一些技术问题,但基本上这两个操作定义了monad。真正的问题是,“monad做什么?”,这取决于monad-列表是monad,Maybes是monad;IO操作是monad。当我们说这些东西是monad时,这意味着它们具有返回和>>=的monad接口。

其他回答

我想说,与monads最接近的OO类比是“命令模式”。

在命令模式中,将普通语句或表达式包装在命令对象中。命令对象公开执行包装语句的执行方法。所以,语句被转换为可以随意传递和执行的第一类对象。可以组合命令,以便通过链接和嵌套命令对象来创建程序对象。

命令由单独的对象调用程序执行。使用命令模式(而不仅仅是执行一系列普通语句)的好处是,不同的调用程序可以将不同的逻辑应用于如何执行命令。

命令模式可用于添加(或删除)宿主语言不支持的语言功能。例如,在没有异常的假设OO语言中,可以通过向命令公开“try”和“throw”方法来添加异常语义。当命令调用throw时,调用程序会回溯到命令列表(或树),直到最后一次“try”调用。相反,您可以通过捕获每个单独命令抛出的所有异常,并将它们转换为错误代码,然后传递给下一个命令,从而从语言中删除异常语义(如果您认为异常是坏的)。

甚至更花哨的执行语义(如事务、非确定性执行或延续)也可以用本机不支持的语言实现。如果你仔细想想,这是一个非常强大的模式。

实际上,命令模式并没有像这样作为通用语言特性使用。将每个语句转换为单独的类的开销将导致无法忍受的样板代码。但原则上,它可以用于解决与在fp中使用monad解决的问题相同的问题。

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

看到我对“什么是monad?”的回答了吗

它从一个激励性的例子开始,通过这个例子,导出一个monad的例子,并正式定义“monad”。

它假设不了解函数式编程,并且使用带有函数(参数):=表达式语法的伪代码和最简单的表达式。

这个C++程序是伪代码monad的一个实现。(仅供参考:M是类型构造函数,feed是“绑定”操作,wrap是“返回”操作。)

#include <iostream>
#include <string>

template <class A> class M
{
public:
    A val;
    std::string messages;
};

template <class A, class B>
M<B> feed(M<B> (*f)(A), M<A> x)
{
    M<B> m = f(x.val);
    m.messages = x.messages + m.messages;
    return m;
}

template <class A>
M<A> wrap(A x)
{
    M<A> m;
    m.val = x;
    m.messages = "";
    return m;
}

class T {};
class U {};
class V {};

M<U> g(V x)
{
    M<U> m;
    m.messages = "called g.\n";
    return m;
}

M<T> f(U x)
{
    M<T> m;
    m.messages = "called f.\n";
    return m;
}

int main()
{
    V x;
    M<T> m = feed(f, feed(g, wrap(x)));
    std::cout << m.messages;
}

我将尝试使用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
}

你最近有一篇演讲《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模型莫纳德计算模型计算的应用模型