Monad用简单的英语?(对于没有FP背景的OOP程序员)


743

用OOP程序员会理解的术语(没有任何函数式编程背景),什么是monad?

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

编辑:

为了阐明我正在寻找的理解类型,假设您正在将具有monad的FP应用程序转换为OOP应用程序。您将如何将monad的责任移植到OOP应用程序?




10
@Pavel:对于有OO背景(相对于FP背景)的人,我们从Eric那里得到的答案比其他建议的Q 答案好得多。
Donal Fellows 2010年

5
@Donal:如果这一个骗子(对此我没有意见),则应在原件上加上好答案。那就是:一个好的答案并不排除重复出现。如果副本足够接近,则可以由主持人作为合并来完成。
dmckee ---前主持人小猫,2010年

Answers:


732

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

用OOP程序员会理解的术语(没有任何函数式编程背景),什么是monad?

monad是一种类型“放大器”,遵循某些规则提供某些操作

首先,什么是“类型放大器”?我的意思是说,有一个系统可以让您选择一种类型并将其转换为更特殊的类型。例如,在C#中考虑Nullable<T>。这是一种放大器。它使您可以使用一个类型,例如int,并为该类型添加新功能,即现在可以在以前无法使用时为null。

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

什么是“某些规则”?简而言之,存在一种合理的方式,使基础类型上的功能在放大类型上起作用,从而使它们遵循功能组成的正常规则。例如,如果您有一个整数函数,请说

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

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

(这是非常模糊和不精确的;您要求提供一种解释,该解释没有假定任何有关功能组成的知识。)

什么是“操作”?

  1. 有一个“单位”操作(有时又称为“返回”操作),该操作从普通类型获取值并创建等效的单价值。本质上,这提供了一种获取未放大类型的值并将其转换为放大类型的值的方法。可以将其实现为OO语言的构造函数。

  2. 有一个“绑定”操作,它接受一个单子值和一个可以转换该值并返回新单子值的函数。绑定是定义monad语义的关键操作。它使我们可以将未放大类型的操作转换为对放大类型的操作,这要遵循前面提到的功能组成规则。

  3. 通常有一种方法可以使未放大类型从放大类型中退回。严格来说,此操作不需要具有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);
}

并通过该属性将其变Nullable<int>回原样。intValue

函数转换是关键。注意,在转换中如何捕获可为空操作的实际语义,即可null传播操作的语义null。我们可以对此进行概括。

假设您具有从int到的功能int,就像我们原来的功能一样M。您可以轻松地将其转换为接受an int并返回a 的函数,Nullable<int>因为您可以通过可为null的构造函数运行结果。现在假设您具有以下高阶方法:

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

看到你能做什么?现在,任何采用an int并返回an int或采用an int并返回a的Nullable<int>方法现在都可以应用可空语义

此外:假设您有两种方法

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

而您想组成它们:

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

即和Z的组成。但是您不能这样做,因为需要一个,然后返回一个。但是,由于您具有“绑定”操作,因此可以进行以下工作:XYXintYNullable<int>

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

对单声道的绑定操作使放大类型上的功能组合起作用。我上面挥手要说的“规则”是:monad保留了正常功能组成的规则。与标识函数组成的结果将产生原始函数,该组成具有关联性,依此类推。

在C#中,“绑定”称为“ 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;            
}

可为空的monad规则是“将产生可为空的两个函数组合在一起,检查内部函数是否为null;如果满足,则产生null,否则为null,然后用结果调用外部函数”。这是可为空的所需语义。

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

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

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;            
}

因此,现在我们可以说:“将这组单个整数放大为整数序列。将该特定整数转换为一串字符串,放大为一系列字符串。现在将这两个操作放在一起:将这一系列整数放大为所有的字符串序列。” Monads使您可以构成放大。

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

这就好比问“单例模式能解决什么问题?”,但我会给它一个机会。

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

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

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

为了阐明我正在寻找的理解类型,假设您正在将具有monad的FP应用程序转换为OOP应用程序。您将如何将Monad的责任移植到OOP应用程序中?

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

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


建议进一步阅读:

  1. 有关C#中monad的更深入和理论上合理的解释,我强烈建议我(Eric Lippert的同事)Wes Dyer撰写有关该主题的文章。本文是monads最终为我“点击”时向我解释的内容。
  2. 一个很好的说明,为什么您可能想要一个monad (在示例中使用Haskell)
  3. 上一篇文章到JavaScript的“翻译”。


17
这是一个很好的答案,但我的头很灰心。我将在本周末跟进并凝视它,并问您问题是否会解决并在我的脑海中说得通。
保罗·内森

5
像往常一样出色的解释Eric。对于更具理论性(但仍然很有趣)的讨论,我发现Bart De Smet在MinLINQ上的博客文章也有助于将某些函数式编程结构也与C#相关联。community.bartdesmet.net/blogs/bart/archive/2010/01/01/…–
罗恩·沃霍尔奇

41
对我来说,说增加类型而不是扩大类型更有意义。
加布

61
@slomojo:我将其改回了我写的打算写的东西。如果您和Gabe想写您自己的答案,那就继续吧。
埃里克·利珀特

24
@Eric,当然由您决定,但是Amplifier意味着现有属性得到了增强,这具有误导性。
ocodo 2010年

341

为什么我们需要单子?

  1. 我们只想使用函数进行编程。(毕竟-FP是“功能性编程”)。
  2. 然后,我们有第一个大问题。这是一个程序:

    f(x) = 2 * x

    g(x,y) = x / y

    我们如何说 要先执行什么?我们如何仅使用函数就可以形成一个有序的函数序列(即程序)?

    解决方案:编写函数。如果先要g然后f再写就可以f(g(x,y))。好的但是 ...

  3. 更多问题:某些功能可能会失败(即g(2,0),除以0)。我们在FP没有“例外”。我们该如何解决?

    解决方案:让函数让函数返回两种东西g : Real,Real -> Real让我们g : Real,Real -> Real | Nothing(函数从两个实数转换为(实数或虚无))而不是让(函数从两个实数转换为实数)。

  4. 但是函数(为了简化)应该只返回一件事

    解决方案:让我们创建一种新的要返回的数据类型,即“ 装箱类型 ”,其中可能包含实数或仅是空数。因此,我们可以拥有g : Real,Real -> Maybe Real。好的但是 ...

  5. 现在会发生什么f(g(x,y))f还没准备好消费Maybe Real。而且,我们不想更改可以连接的每个函数g来消耗Maybe Real

    解决方案:让我们有一个特殊的功能来“连接” /“组合” /“链接”功能。这样,我们可以在幕后调整一项功能的输出以提供下一项功能。

    在我们的例子中:( g >>= f连接/组成gf)。我们要>>=获取g的输出,对其进行检查,以防万一它Nothing不调用f并返回Nothing;或者相反,提取盒装Realf用它喂食。(此算法只是>>=针对该Maybe类型的实现)。

  6. 使用相同的模式可以解决许多其他问题:1.使用“框”来编纂/存储不同的含义/值,并具有g返回这些“框值”的函数。2.让作曲者/链接g >>= f者帮助将g的输出连接到f的输入,因此我们完全不需要更改f

  7. 使用此技术可以解决的显着问题是:

    • 具有全局状态,函数序列中的每个函数(“程序”)可以共享:solution StateMonad

    • 我们不喜欢“不纯函数”:对于相同输入产生不同输出的函数。因此,让我们标记这些函数,使它们返回标记/装箱的值:monad。IO

完全幸福!


2
据我所知,@DmitriZaitsev异常只能在“不纯代码”(IO monad)中发生。
cibercitizen1'1

3
@DmitriZaitsev任何其他类型(与预期的Real均不同)可以扮演Nothing的角色。那不是重点。在该示例中,问题在于当前一个返回一个意外值类型而后一个而不链接后一个(仅接受Real作为输入)时,如何在链中调整函数。
cibercitizen1'1

3
另一个令人困惑的问题是,“ monad”一词在您的答案中仅出现两次,并且仅与其他术语组合使用- StateIO,并且都没有给出“ monad”的确切含义
Dmitri Zaitsev

31
对我来说,作为一个来自OOP背景的人,这个答案确实很好地解释了拥有monad的动机以及monad的实际含义(远远超过了公认的答案)。因此,我发现它非常有帮助。非常感谢@ cibercitizen1和+1
akhilless

3
我一直在阅读有关断断续续的函数式编程的文章,已有大约一年的时间。这个答案,尤其是前两点,最终使我理解了命令式编程的真正含义,以及为什么函数式编程是不同的。谢谢!
jrahhali '18

82

我会说与单子最接近的OO类比是“ 命令模式 ”。

在命令模式中,将普通语句或表达式包装在命令对象中。该命令对象公开了一个执行方法,该方法执行包装的语句。因此,语句变成了可以随意传递和执行的一流对象。可以组合命令,因此您可以通过链接和嵌套命令对象来创建程序对象。

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

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

像这样的更加花哨的执行语义,例如事务,非确定性执行或连续性,都可以用本机不支持的语言来实现。如果您考虑一下,这是一个非常强大的模式。

现在,实际上,命令模式并未像这样被用作通用语言功能。将每个语句转换为单独的类的开销将导致大量的样板代码。但是从原理上讲,它可以用来解决与单子在fp中解决相同的问题。


15
我相信这是我见过的第一个对monad的解释,它不依赖于函数编程概念,而是用真实的OOP术语来解释。真的很好的答案。
David K. Hess

这与FP / Haskell中的monad实际上非常接近,除了命令对象本身“知道”它们所属的“调用逻辑”(并且只能将兼容对象链接在一起)之外;调用程序只提供第一个值。它不是可以通过“非确定性执行逻辑”来执行“打印”命令。不,它必须是“ I / O逻辑”(即IO monad)。但是除此之外,它非常接近。您甚至可以说Monad只是程序(由代码语句构建,稍后将执行)。在早期,“ bind”被称为“可编程分号”
Will Ness

1
@ DavidK.Hess我确实非常怀疑使用FP解释基本FP概念的答案,尤其是使用FP语言(例如Scala)的答案。干得好,雅克B!
恢复莫妮卡

62

用OOP程序员会理解的术语(没有任何函数式编程背景),什么是monad?

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

就OO编程而言,monad是一个接口(或更可能是一个mixin),由一种类型的参数化,具有两种方法,returnbind描述:

  • 如何注入一个值以获得该注入值类型的单子值;
  • 如何使用将非单值变成单值的函数。

它解决的问题是您从任何接口期望得到的相同类型的问题,即:“我有很多不同的类,它们执行不同的操作,但是似乎以具有根本相似性的方式来执行这些不同的操作。我可以描述它们之间的相似性,即使类本身不是'Object'类本身的真正子类型呢?”

更具体地,Monad“接口”类似于IEnumeratorIIterator采用其自身接受类型的类型。不过,主要的“要点” Monad是能够连接基于内部类型的操作,甚至可以连接到具有新的“内部类型”的要点,同时保持甚至增强主要类的信息结构。


1
return实际上不是monad上的方法,因为它没有monad实例作为参数。(即:没有这个/自己)
劳伦斯·贡萨尔维斯

@LaurenceGonsalves:由于我目前正在研究本科学位论文,因此我认为主要的限制是C#/ Java接口中缺少静态方法。您可以在实现整个monad故事的方向上走得很远,至少是静态绑定而不是基于类型类。有趣的是,尽管缺少更高种类的类型,它甚至仍然可以工作。
塞巴斯蒂安·格拉夫

42

您最近有克里斯托弗·联盟Christopher League)的演讲“ Monadologie-缓解类型焦虑的专业帮助 ” (2010年7月12日),该主题在延续和monad方面非常有趣。 与此演示文稿一起播放的视频实际上可在 vimeo上获得。 在这段1小时的视频中,Monad部分的开始时间约为37分钟,从58张幻灯片的幻灯片42开始。

它被表示为“函数式编程的领先设计模式”,但是示例中使用的语言是Scala,它既是OOP又是函数式的。
您可以从Debasish Ghosh(2008年3月27日)的博客文章“ Monads- Scala中抽象计算的另一种方法 ”中了解有关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中):

  • Option 是单子
    def单位[A](x:A):选项[A] = Some(x)

    def flatMap [A​​,B](m:Option [A])(f:A => Option [B]):Option [B] =
      m匹配{
       case无=>无
       情况Some(x)=> f(x)
      }
  • List 是莫纳德
    def单位[A](x:A):列表[A] =列表(x)

    def flatMap [A​​,B](m:List [A])(f:A => List [B]):List [B] =
      m匹配{
        情况Nil => Nil
        案例x :: xs => f(x)::: flatMap(xs)(f)
      }

Monad在Scala中很重要,因为使用Monad结构构建了方便的语法:

for对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[AnotherType]并返回List[AnotherType]。需要注意的重要一点是,所有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则可以像所有操作一样使用实际的类型(VenueUser),并使Option验证内容保持隐藏,这都是因为for语法的平面图:

val confirm = for {
  venue <- lookupVenue(name)
  user <- getLoggedInUser(session)
  confno <- reserveTable(venue, user)
} yield {
  mailTo(confno, user)
  confno
}

仅当所有三个功能都具有时Some[X],yield部分才会执行;任何None都将直接返回给confirm


所以:

Monad允许在函数式编程中进行有序的计算,这使我们能够以一种很好的结构化形式(类似于DSL)来对动作排序进行建模。

强大的功能来自将服务于不同目的的monad组合成应用程序中可扩展的抽象的能力。

由monad进行的动作排序和线程化是由语言编译器完成的,该语言编译器通过闭包的魔术进行转换。


顺便说一下,Monad不仅是FP中使用的计算模型:

范畴理论提出了许多计算模型。其中

  • 计算的箭头模型
  • Monad计算模型
  • 计算的应用模型

2
我喜欢这个解释!您提供的示例很好地演示了该概念,还添加了Eric预告片中有关SelectMany()是Monad的IMHO缺少的内容。谢谢!
2011年

1
恕我直言,这是最优雅的答案
Polymerase

还有其他一切,Functor。
尼斯

34

为了尊重快速的读者,我首先从精确的定义开始,继续快速地进行更多的“普通英语”解释,然后再转向示例。

这是一个简洁而精确的定义,略有修改:

一个单子(计算机科学)是正式的地图:

  • X某种给定编程语言的每种类型发送到一种新的类型T(X)(称为“-的类型,T其值以X”表示);

  • 配备有用于构成形式的两个函数的规则 f:X->T(Y)g:Y->T(Z)到功能g∘f:X->T(Z);

  • 以某种方式在明显的意义上与给定的称为的给定单位函数相关联pure_X:X->T(X),被认为是将一个值带到只返回该值的纯计算中。

因此,在简单的话,一个单子从任何类型的传递规则X为另一种类型T(X),并且规则从两个函数传递f:X->T(Y)g:Y->T(Z)(你想撰写,但不能)到一个新的功能h:X->T(Z)。但是,这并不是严格意义上的数学构成。我们基本上是在“弯曲”功能的组成或重新定义功能的组成方式。

另外,我们要求构成monad的规则可以满足“显而易见的”数学公理:

  • 关联:撰写fg,然后用h(从外部)应该是相同的,作为构成gh,然后与f(从内侧)。
  • 单位性f用双方的身份函数构成f

再次,用简单的话来说,我们不能随心所欲地重新定义我们喜欢的函数组成:

  • 我们首先需要关联性,以便能够连续地组合多个函数,例如f(g(h(k(x))),而不必担心指定组成函数对的顺序。由于monad规则仅规定了如何组成一对函数,而没有该公理,我们将需要知道哪个对首先组成,依此类推。(请注意,不同于可交换属性不同f组成与g的相同。g与组成f,这不是必需的)。
  • 其次,我们需要单位属性,这就是说身份按照我们期望的方式琐碎地组成。因此,只要可以提取这些身份,我们就可以安全地重构函数。

再次简单地讲:monad是类型扩展和组成满足两个公理(关联性和单位属性)的函数的规则。

实际上,您希望monad由可以为您组成功能的语言,编译器或框架来实现。因此,您可以专注于编写函数的逻辑,而不必担心如何实现它们的执行。

简而言之,就是如此。


作为专业的数学家,我宁愿避免调用h了“成分” fg。因为从数学上来说不是。称其为“组成”是错误的假设,h即不是真正的数学组成。它甚至不是由f和唯一确定的g。相反,这是我们monad新的“组成规则”功能的结果。即使实际的数学构成存在,这也可能与实际的数学构成完全不同!


为了减少干燥,让我尝试通过示例进行说明,以小节进行注释,因此您可以直接跳过这一点。

抛出异常作为Monad示例

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

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

但是f(0)未定义,因此e引发异常。那如何定义构图价值g(f(0))呢?当然,再次抛出异常!也许是一样的e。也许是新的更新异常e1

这里到底发生了什么?首先,我们需要新的异常值(不同或相同)。您可以调用它们nothingnull其他任何东西,但本质保持不变-它们应该是新值,例如,number在我们的示例中,它不应该是a 。我不想给他们打电话null,以免与如何null以任何特定语言实现混淆。同样,我更喜欢避免,nothing因为它通常与关联null,从原则上讲,null应该这样做,但是,无论出于何种实际原因,该原则通常都会被屈服。

到底什么是例外?

对于任何经验丰富的程序员来说,这都是微不足道的事情,但是我想说几句话只是为了消除任何混乱的蠕虫:

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

范围包括丢弃任何详细信息并返回单个全局值(如NaNnull)或生成长日志列表或确切发生的事情,将其发送到数据库并在整个分布式数据存储层进行复制;)

这两个极端的例外示例之间的重要区别在于,在第一种情况下,没有副作用。在第二个中。这使我们想到了(千美元)问题:

纯函数中是否允许例外?

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

更长的答案。纯粹来说,函数的输出必须由其输入唯一地确定。因此,我们f通过发送0e我们称为异常的新抽象值来修改函数。我们确保该值不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之类的语言中被弄乱了,其中error不是一种类型在我们的函数组成方面表现出我们想要的方式,而实际类型却表现出nullNaN不表现出这种方式,而是经历了一些虚假且并不总是直观的行为类型转换。

类型扩展

当我们想改变异常中的消息时,我们实际上是E在为整个异常对象声明一个新类型,然后maybe number,除了它令人困惑的名称(该类型number是新异常类型还是该新异常类型)之外,这就是它的作用。E,因此它实际上number | Enumber和的结合E。特别是,它取决于我们要如何构造E,该名称既未建议也未反映在名称中maybe number

什么是功能成分?

这是取函数f: X -> Y并将g: Y -> Z其组成构造为函数h: X -> Z满足的数学运算 h(x) = g(f(x))。当f(x)不允许将结果用作参数时,会发生此定义的问题g

在数学中,如果没有额外的工作,这些功能就无法组成。对于上述f和的示例,严格的数学解决方案g0从的定义集中删除f。有了新的定义集(的新的限制性更强的类型x),f它就可以与组合g

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

或作为另一种方法,都像是人为制造的值NaNundefinednullInfinity等于是你评估1/0Infinity1/-0-Infinity。然后将新值强制返回到表达式中,而不是引发异常。导致结果的您可能会或可能无法预测:

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

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

JavaScript使我们能够不惜一切代价继续执行数值表达式,而不会像上面的示例中那样引发错误。这意味着,它还可以组成功能。这正是monad的含义-构成满足此答案开头定义的公理的函数是规则。

但是,构成函数的规则是否源于JavaScript用于处理数字错误的实现,而又是monad?

要回答这个问题,您所需要的只是检查公理(此处不作为练习的一部分保留为练习;)。

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

确实,一个更有用的monad会是一条规则,规定如果f对某些对象抛出异常x,则它与any的组合也是如此g。另外,使异常E仅在全局范围内唯一,并且只有一个可能的值(类别理论中的终端对象)。现在,这两个公理立即可检查,我们得到了一个非常有用的单子。结果就是众所周知的monad


3
贡献良多。+1但也许您想删除“最长的发现大多数解释……”。其他人将根据需要判断是否是“普通英语”的问题:“用简单的词,以一种简单的方式使用普通英语==”。
cibercitizen16年

@ cibercitizen1谢谢!如果您不计算示例,它实际上很短。最主要的一点是,您无需阅读示例即可了解其定义。不幸的是,许多解释迫使我先阅读示例,这通常是不必要的,但是,当然,可能需要作者多做一些工作。由于过于依赖特定示例,因此存在这样的危险,即无关紧要的细节会掩盖图片并使其难以掌握。话虽如此,您有有效的观点,请参阅更新。
德米特里·扎伊采夫

2
太长且令人困惑
-seenimurugan

1
@seenimurugan欢迎提出改进建议;)
Dmitri Zaitsev

26

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

  • return x 创建一个封装的monad类型的值 x
  • m >>= f(将其读取为“ bind运算符”)将函数应用于fmonad中的值m

那就是一个单子。还有其他一些技术,但是基本上这两个操作定义了monad。真正的问题是,“一个monad 什么?”,这取决于monad-列表是monad,Maybes是monad,IO操作是monad。所有这一切,当我们说这些东西的单子的意思是,他们拥有的单子接口return>>=


“一个monad做什么,这取决于monad”:更确切地说,取决于bind每个monadic类型必须定义的功能,不是吗?这是不将绑定与组合混淆的一个很好的理由,因为组合只有一个定义,而绑定函数不能只有一个定义,如果我正确理解的话,每个单子类型都有一个定义。
Hibou57年

14

来自维基百科

在函数式编程中,monad是一种用于表示计算(而不是域模型中的数据)的抽象数据类型。Monad使程序员可以将动作链接在一起以构建管道,其中每个动作都装饰有monad提供的其他处理规则。以功能风格编写的程序可以使用monad来构建包括顺序操作1 [2]的过程,或者定义任意控制流(如处理并发,连续或异常)。

形式上,一个monad是通过定义两个操作(绑定和返回)和一个类型构造函数M来构造的,该构造函数必须满足几个属性才能正确组成monadic函数(即,使用monad中的值作为参数的函数)。返回操作从普通类型中获取一个值,并将其放入类型M的一元容器中。bind操作执行相反的过程,从容器中提取原始值,并将其传递给管道中关联的下一个函数。

程序员将编写单子函数来定义数据处理管道。monad充​​当框架,因为它是一种可重用的行为,它决定调用管道中特定monadic函数的顺序,并管理计算所需的所有秘密工作。[3] 在每个monadic函数返回控制后,将执行流水线中交错的bind和return运算符,并将照顾monad处理的特定方面。

我相信它可以很好地解释它。


12

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

例子

List 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以及Promises和Tasks也可以设置为monadic。

评论

Monad并不那么复杂。该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()(此类方法是允许的扩展)

注意:函数语言允许您编写可以在任何类型的monadic通用类上运行的函数。为此,必须编写用于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
}

7

monad在OO中是否具有“自然”的解释取决于monad。在Java之类的语言中,您可以将monad转换为检查空指针的语言,从而使失败的计算(即,在Haskell中不产生任何结果)发出空指针作为结果。您可以将状态monad转换为通过创建可变变量和更改其状态的方法生成的语言。

一元论是endofunctors类别中的一个monoid。

句子汇总的信息非常深刻。您可以使用任何命令式语言在monad中工作。monad是一种“排序的”领域特定语言。它满足某些有趣的特性,这些特性使monad成为“命令式编程”的数学模型。Haskell使定义小型(或大型)命令式语言变得容易,这些语言可以通过多种方式组合。

作为OO程序员,您使用语言的类层次结构来组织可以在上下文中称为对象的函数或过程的种类。只要可以以任意方式组合不同的monad,将monad的所有方法有效地“导入”到范围内,monad也是该想法的抽象。

在架构上,然后使用类型签名来明确表示哪些上下文可用于计算值。

为此,可以使用monad变压器,并且所有“标准” monad都有高质量的集合:

  • 列表(非确定性计算,通过将列表视为域)
  • 可能(计算可能会失败,但报告不重要)
  • 错误(可能失败并需要异常处理的计算
  • 阅读器(可以由简单的Haskell函数的组合表示的计算)
  • 编写器(具有顺序“呈现” /“记录”(到字符串,html等)的计算
  • 续(续)
  • IO(取决于基础计算机系统的计算)
  • 状态(上下文中包含可修改值的计算)

与相应的monad转换器和类型类。类型类提供了一种补充方法,可以通过统一单子接口来组合单子,以便具体的单子可以为单子“种类”实现标准接口。例如,模块Control.Monad.State包含类MonadState sm,而(State s)是以下形式的实例

instance MonadState s (State s) where
    put = ...
    get = ...

长话大说,monad是一个将“上下文”附加到值的函子,至少可以将一个值注入monad,并且可以根据其附加的上下文来评估值。以受限的方式。

所以:

return :: a -> m a

是将a类型的值注入m a类型的monad“操作”的函数。

(>>=) :: m a -> (a -> m b) -> m b

是执行monad操作,评估其结果并将功能应用于结果的函数。关于(>> =)的一件整洁的事情是结果在同一单子中。换句话说,在m >> = f中,(>> =)将结果从m中拉出,并将其绑定到f,这样结果就在单子中。(或者,我们可以说(>> =)将f拉入m并将其应用于结果。)因此,如果我们有f :: a-> mb和g :: b-> mc,我们可以“序列”动作:

m >>= f >>= g

或者,使用“做记号”

do x <- m
   y <- f x
   g y

(>>)的类型可能是发光的。它是

(>>) :: m a -> m b -> m b

它对应于过程语言(如C)中的(;)运算符。它允许使用do表示法,例如:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn

在数学和哲学逻辑中,我们有框架和模型,这些框架和模型通过单子论“自然”建模。解释是一种函数,它研究模型的域并计算命题(或公式,在归纳法下)的真值(或归纳)。在必要性的模态逻辑中,我们可以说,如果一个命题在“每个可能的世界”中都是正确的,则该命题是必要的-如果在每个允许的领域中都是正确的。这意味着可以将命题语言中的模型重新定义为一个模型,该模型的域包括不同模型的集合(每个模型对应于每个可能的世界)。每个monad都有一个名为“ join”的方法,它可以使层变平,这意味着结果为monad动作的每个monad动作都可以嵌入到monad中。

join :: m (m a) -> m a

更重要的是,这意味着monad在“ layer stacking”操作下是关闭的。这就是monad转换器的工作方式:它们通过为类似类型的类型提供“ join-like”方法来组合monad

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

这样我们就可以将(MaybeT m)中的动作转换为m中的动作,有效地折叠图层。在这种情况下,runMaybeT :: MaybeT ma-> m(Maybe a)是我们类似join的方法。(MaybeT m)是monad,而MaybeT :: m(Maybe a)-> MaybeT ma实际上是m中新型monad动作的构造函数。

函子的自由monad是通过堆叠f生成的monad,这意味着f的每个构造函数序列都是自由monad的元素(或更准确地说,形状与该构造函数的序列树相同的形状)。 F)。免费的monad是用最少的样板构建灵活的monad的有用技术。在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))

Monadism是您可能称为“解释器”或“命令”模式的基础体系结构,抽象为最清晰的形式,因为每个monadic计算都必须“运行”,至少是微不足道的。(运行时系统为我们运行IO monad,并且是任何Haskell程序的入口点。IO通过依次运行IO操作来“驱动”其余计算。)

join的类型也是在该语句中,monad是endofunctors类别中的一个monoid的语句。由于其类型,联接通常对于理论目的更为重要。但是了解类型意味着了解单子。从功能组成的意义上讲,Join和monad变换器的join-like类型实际上是endofunctors的组成。放在类似Haskell的伪语言中,

Foo :: m(ma)<->(m。m)一个


3

单子是一组功能

(Pst:函数数组只是一个计算)。

实际上,不是由一个真正的数组(一个单元格数组中的一个函数)而是由另一个函数>> =链接的那些函数。>> =可以使函数i的结果适应于函数i + 1,在它们之间进行计算,甚至不调用函数i + 1。

此处使用的类型是“具有上下文的类型”。这是带有“标签”的值。被链接的函数必须采用“裸值”并返回标记的结果。>> =的职责之一是从其上下文中提取裸值。还有一个函数“ return”,它带有一个裸值并带有标签。

Maybe的一个例子。让我们使用它来存储用于进行计算的简单整数。

-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return  (a*b)

-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom

-- tagged value
val1 = Just 160 

-- array of functions feeded with val1
array1 = val1 >>= divideBy 2  >>= multiply 3 >>= divideBy  4 >>= multiply 3

-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
       v <- divideBy 2  n
       v <- multiply 3 v
       v <- divideBy 4 v
       v <- multiply 3 v
       return v

-- array of functions, 
-- the first >>= performs 160 / 0, returning Nothing
-- the second >>= has to perform Nothing >>= multiply 3 ....
-- and simply returns Nothing without calling multiply 3 ....
array3 = val1 >>= divideBy 0  >>= multiply 3 >>= divideBy  4 >>= multiply 3

main = do
     print array1
     print (array2 160)
     print array3

仅为了说明monad是具有助手操作的函数数组,请考虑与上述示例等效,仅使用一个真实的函数数组

type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions

myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]

-- function for the machinery of executing each function i with the result provided by function i-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs

它会像这样使用:

print (runMyMonad (Just 160) myArray1)

1
超级整洁!因此,bind只是一种在具有上下文的输入上依次评估具有上下文的功能数组的方法:)
Musa Al-hassy 2014年

>>=是运算符
user2418306'1

1
我认为“函数数组”的类比并没有阐明太多。如果\x -> x >>= k >>= l >>= m是一个函数数组,则是h . g . f,它根本不涉及monad。
duplode '16

我们可以说函子,无论是单子函数,应用函数还是简单函数,都是关于“被套用的”。“应用”添加链接,“单子”添加依赖关系(即,根据上一个计算步骤的结果创建下一个计算步骤)。
尼斯将于

3

用面向对象的术语来说,monad是一个流畅的容器。

最低要求是对定义的class <A> Something支持,该定义支持构造函数Something(A a)和至少一个方法Something<B> flatMap(Function<A, Something<B>>)

可以说,它也可以算出monad类是否具有任何带有签名的方法Something<B> work()来保留类的规则-编译器在编译时会在flatMap中进行烘焙。

为什么monad有用?因为它是一个容器,允许保留语义的可链接操作。例如,Optional<?>保留isPresent为语义Optional<String>Optional<Integer>Optional<MyClass>等。

作为一个粗略的例子,

Something<Integer> i = new Something("a")
  .flatMap(doOneThing)
  .flatMap(doAnother)
  .flatMap(toInt)

请注意,我们以字符串开头,以整数结尾。很酷

在OO中,可能需要花费一些时间,但是Something上任何返回Something的另一个子类的方法都符合返回原始类型的容器的容器函数的标准。

这就是您保留语义的方式-即容器的含义和操作没有改变,它们只是包装和增强了容器内的对象。


2

典型用法中的Monad在功能上等同于过程编程的异常处理机制。

在现代过程语言中,您将异常处理程序放在一系列语句周围,任何一条语句都可能引发异常。如果任何一条语句引发异常,则该语句序列的正常执行将停止并转移到异常处理程序。

但是,功能性编程语言从哲学上避免了由于异常性质而引起的“ goto”异常处理功能。从功能编程的角度来看,功能不应具有“副作用”,例如破坏程序流程的异常。

实际上,主要由于I / O不能排除现实世界中的副作用。函数编程中的Monad通过处理一组链接的函数调用(其中任何一个都可能产生意外的结果)并将任何意外的结果转换为封装的数据,从而仍然可以安全地流经其余的函数调用来处理此问题。

控制流得以保留,但意外事件被安全地封装和处理。


2

这里有一个有关Monvel案例研究的简单Monads解释。

Monad是用于对有效的依赖函数进行排序的抽象。这里的有效意味着它们以F [A]形式返回一个类型,例如Option [A],其中Option为F,称为类型构造函数。让我们通过2个简单的步骤看一下

  1. 下面的功能组合是可传递的。因此,从A到CI可以组成A => B和B =>C。
 A => C   =   A => B  andThen  B => C

在此处输入图片说明

  1. 但是,如果函数返回的效果类型为Option [A],即A => F [B],则该组合对于转到B无效,我们需要A => B,但我们需要A => F [B]。
    在此处输入图片说明

    我们需要一个特殊的运算符“ bind”,该运算符知道如何融合这些返回F [A]的函数。

 A => F[C]   =   A => F[B]  bind  B => F[C]

“绑定”功能是用于在特定定义˚F

还为任何A定义了A => F [A]类型的“返回”,也为该特定F定义了“返回”。要成为Monad,F必须为其定义这两个功能。

因此,我们可以从任何纯函数A => B构造一个有效函数A => F [B]

 A => F[B]   =   A => B  andThen  return

但是给定的F也可以定义自己的不透明的“内置”特殊功能,此类功能使用户无法自行定义(使用语言),例如

  • “ random”(范围=> Random [Int]
  • “ print”(字符串=> IO [()]
  • “尝试...抓住”等

2

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



1

请参阅我对“什么是单子”的回答

它从一个具有启发性的示例开始,通过示例进行工作,派生出monad的示例,并正式定义“ monad”。

它假定不具备任何函数式编程知识,并且使用带有function(argument) := expression语法的伪代码和最简单的表达式。

此C ++程序是伪代码monad的实现。(供参考:M是类型构造函数,feed是“ bind”操作,wrap是“ return”操作。)

#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;
}

0

从实用的角度(总结许多先前的答案和相关文章中所讲的内容),在我看来,monad的基本“目的”(或有用性)之一是利用递归方法调用中隐含的依赖项。 aka函数组合(即,当f1调用f2调用f3时,需要在f2之前先评估f3在f1之前)以自然方式表示顺序组合,特别是在惰性评估模型的情况下(即顺序组合为纯序列) ,例如C中的“ f3(); f2(); f1();”-如果您想到f3,f2和f1实际上不返回任何情况的情况,则技巧尤为明显[它们的链接为f1(f2(f3)))是人工的,纯粹是为了创建序列])。

当涉及到副作用时,这尤其相关,例如,当某些状态发生变化时(如果f1,f2,f3没有副作用,则以什么顺序评估它们都无关紧要;这是pure的一个重要特性功能语言,以便能够并行化这些计算)。纯功能越多越好。

我认为从那种狭narrow的角度来看,对于那些倾向于惰性评估(仅在绝对必要时才进行评估,并且遵循不依赖于代码表示的顺序进行评估的语言)的语言,monad可以看作是语法糖。表示顺序组成的其他方式。最终结果是,可以以强制性的方式自然地呈现“不纯”(即确实具有副作用)的代码部分,而将它们与纯函数(没有副作用)清晰地分开。懒洋洋地评价。

这仅仅是一个方面,虽然,作为警告这里


0

我能想到的最简单的解释是,单子是将函数与具有良好结果的函数(也称为Kleisli合成)进行组合的一种方式。“嵌入的”函数具有签名a -> (b, smth),其中ab分别是类型(如IntBool),它们可能彼此不同,但不一定是不同的-并且smth是“上下文”或“嵌入”。

这种类型的功能也可以写入a -> m b其中m是相当于“embelishment” smth。因此,这些函数是在上下文中返回值的函数(想想记录它们的动作的函数,smth记录消息在哪里;或者执行输入\输出的函数,其结果取决于IO动作的结果)。

monad是一个接口(“ typeclass”),它使实现者告诉其如何组成此类功能。实现者需要为想要实现接口的(a -> m b) -> (b -> m c) -> (a -> m c)任何类型定义一个合成函数m(这是Kleisli合成)。

所以,如果我们说我们有一个元组类型(Int, String)较上计算的结果Ints表示也记录自己的操作,与(_, String)作为“embelishment” -操作的日志-和两个函数increment :: Int -> (Int, String)twoTimes :: Int -> (Int, String)我们想获得一个功能incrementThenDouble :: Int -> (Int, String)是组成这两个功能中的一个也考虑了日志。

在给定的示例中,这两个函数的monad实现适用于整数值2 incrementThenDouble 2(等于twoTimes (increment 2)),如果(6, " Adding 1. Doubling 3.")中间结果increment 2等于(3, " Adding 1.")twoTimes 3等于,则返回(6, " Doubling 3.")

从这一克莱斯里合成函数可以推导出通常的一元函数。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.