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

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

使现代化

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


当前回答

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

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

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

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

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

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

其他回答

来自维基百科:

在函数式编程中,monad是一种抽象数据类型,用于表示计算(而不是域模型中的数据)。Monads公司允许程序员链接动作一起构建管道,其中每个动作都用提供了其他处理规则莫纳德。编写的程序功能性风格可以利用monads来构造程序包括顺序操作,1[2]或定义任意控制流(如处理并发,延续或例外)。形式上,monad由定义两个操作(bind和return)和类型构造函数M必须满足几个财产才能允许正确组成一元函数(即使用monad中的值作为参数)。返回操作需要一个普通类型的值,并将其放入装入M型一元容器中。绑定操作执行反向处理,提取集装箱的原始价值,以及将其传递给关联的下一个函数。程序员将编写monadic定义数据处理的函数管道monad充当框架,因为它是一种可重用的行为这决定了调用管道,并管理所有需要的卧底工作计算[3] 绑定和返回管道中交错的运算符将在每个monadic之后执行函数返回控制,并将注意特定方面由monad处理。

我相信这很好地解释了这一点。

更新:这个问题是一个非常长的博客系列的主题,你可以在Monads上阅读-谢谢你提出的问题!

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

monad是一种类型的“放大器”,它遵守某些规则,并提供某些操作。

首先,什么是“类型放大器”?我指的是某种系统,它可以让你选择一种类型,并将其转换为更特殊的类型。例如,在C#中,考虑Nullable<T>。这是一种类型的放大器。它允许您接受一个类型,比如int,并为该类型添加一个新的功能,即现在它可以在以前不能为null时为null。

作为第二个例子,考虑IEnumerable<T>。这是一种类型的放大器。它允许您获取一个类型,例如字符串,并为该类型添加一个新功能,即您现在可以从任意数量的单个字符串中创建一个字符串序列。

什么是“特定规则”?简言之,对于基础类型上的函数来说,有一种合理的方法来处理放大的类型,从而使它们遵循函数组合的正常规则。例如,如果你有一个关于整数的函数,比如

int M(int x) { return x + N(x * 2); }

则Nullable<int>上的相应函数可以使其中的所有运算符和调用“以与之前相同的方式”一起工作。

(这是难以置信的模糊和不精确;你要求的解释没有假设任何关于功能成分的知识。)

什么是“操作”?

有一个“单元”操作(有时被混淆地称为“返回”操作),它从普通类型中获取值并创建等效的一元值。本质上,这提供了一种获取未放大类型值并将其转换为放大类型值的方法。它可以作为OO语言中的构造函数实现。有一个“绑定”操作,它接受一个一元值和一个可以转换该值的函数,并返回一个新的一元值。Bind是定义monad语义的关键操作。它允许我们将未放大类型上的操作转换为放大类型的操作,这符合前面提到的函数组合规则。通常有一种方法可以使未放大的类型从放大的类型中恢复出来。严格来说,这个操作不需要有monad。(虽然如果你想有一个伴侣,这是必要的。我们不会在本文中进一步考虑这些。)

同样,以Nullable<T>为例。可以使用构造函数将int转换为Nullable<int>。C#编译器为您处理大多数可为空的“提升”,但如果没有,提升转换很简单:,

int M(int x) { whatever }

转化为

Nullable<int> M(Nullable<int> x) 
{ 
    if (x == null) 
        return null; 
    else 
        return new Nullable<int>(whatever);
}

通过Value属性可以将Nullable<int>转换回int。

关键是函数转换。请注意,在转换中如何捕获可为null的操作的实际语义(即对null的操作传播null)。我们可以概括这一点。

假设你有一个从int到int的函数,就像我们最初的M一样。你可以很容易地将它转换成一个接受int并返回Nullable<int>的函数,因为你可以通过可空构造函数来运行结果。现在假设你有一个更高阶的方法:

static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
    if (amplified == null) 
        return null;
    else
        return func(amplified.Value);
}

看看你能用它做什么?任何接受一个int并返回一个int,或者接受一个整型并返回一一个Nullable<int>的方法现在都可以应用可空语义。

此外:假设您有两种方法

Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }

并且您希望编写它们:

Nullable<int> Z(int s) { return X(Y(s)); }

也就是说,Z是X和Y的合成,但不能这样做,因为X取一个int,Y返回一个Nullable<int>。但是,由于您有“绑定”操作,因此您可以执行以下操作:

Nullable<int> Z(int s) { return Bind(Y(s), X); }

monad上的绑定操作使放大类型上的函数组合工作。我在上面写的“规则”是,monad保留了正常函数组合的规则;使用同一函数的合成产生原始函数,合成是关联的,等等。

在C#中,“Bind”被称为“SelectMany”。看看它是如何在序列monad上工作的。我们需要做两件事:将一个值转换为一个序列,并在序列上绑定操作。作为奖励,我们还可以“将序列转换回值”。这些操作包括:

static IEnumerable<T> MakeSequence<T>(T item)
{
    yield return item;
}
// Extract a value
static T First<T>(IEnumerable<T> sequence)
{
    // let's just take the first one
    foreach(T item in sequence) return item; 
    throw new Exception("No first item");
}
// "Bind" is called "SelectMany"
static IEnumerable<T> SelectMany<T>(IEnumerable<T> seq, Func<T, IEnumerable<T>> func)
{
    foreach(T item in seq)
        foreach(T result in func(item))
            yield return result;            
}

可为null的monad规则是“将产生可为null值的两个函数组合在一起,检查内部函数是否产生null值;如果产生null值,则产生null值。如果没有,则调用外部函数产生null值”。这就是nullable的理想语义。

序列monad规则是“将两个生成序列的函数组合在一起,将外部函数应用于内部函数生成的每个元素,然后将所有生成的序列连接在一起”。monad的基本语义在Bind/SelectMany方法中被捕获;这是告诉monad真正含义的方法。

我们可以做得更好。假设您有一个int序列,以及一个接受int并生成字符串序列的方法。我们可以推广绑定操作,以允许组合接受和返回不同放大类型的函数,只要其中一个的输入与另一个的输出匹配:

static IEnumerable<U> SelectMany<T,U>(IEnumerable<T> seq, Func<T, IEnumerable<U>> func)
{
    foreach(T item in seq)
        foreach(U result in func(item))
            yield return result;            
}

现在我们可以说“将这一组单独的整数放大成一系列整数。将这一特定的整数转换成一系列字符串,放大成一组字符串。现在将这两个操作放在一起:将这一系列整数放大成所有字符串序列的串联。”单子允许您合成放大。

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

这相当于问“单例模式解决了什么问题?”,但我会尝试一下。

Monad通常用于解决以下问题:

我需要为这种类型创建新的功能,并且仍然在这种类型上组合旧的功能以使用新的功能。我需要在类型上捕获一堆操作,并将这些操作表示为可组合对象,构建越来越大的组合,直到表示出正确的一系列操作,然后我需要开始获得结果我需要用讨厌副作用的语言清晰地表示副作用操作

C#在设计中使用monad。如前所述,可空模式与“可能是monad”非常相似。LINQ完全由monad构建;SelectMany方法是操作组合的语义工作。(Erik Meijer喜欢指出,每个LINQ函数实际上都可以由SelectMany实现;其他一切都只是为了方便。)

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

大多数OOP语言没有足够丰富的类型系统来直接表示monad模式本身;您需要一个支持高于泛型类型的类型的类型系统。所以我不会这么做。相反,我将实现表示每个monad的泛型类型,并实现表示您需要的三个操作的方法:将值转换为放大值,(可能)将放大值转换为值,以及将未放大值上的函数转换为放大的值上的函数。

一个好的开始是我们如何在C#中实现LINQ。研究SelectMany方法;这是理解序列monad如何在C#中工作的关键。这是一个非常简单的方法,但非常强大!


建议进一步阅读:

为了对C#中的单子进行更深入、理论上更合理的解释,我强烈推荐我(埃里克·里佩尔)的同事韦斯·戴尔(Wes Dyer)关于这个主题的文章。这篇文章是当他们最终为我“点击”时向我解释单子的原因。蒙得斯的奇迹这是一个很好的例子,说明了为什么您可能需要一个monad(在示例中使用Haskell)。你本可以发明修道院的!(也许你已经有了。)丹·皮波尼有点像,将上一篇文章“翻译”为JavaScript。詹姆斯·科格兰(James Coglan)所读过的monads最佳简介精选部分从Haskell翻译成JavaScript


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

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

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

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

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

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

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

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。其结果就是众所周知的可能是莫纳德。