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

705 oop monads functional-programming

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

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

编辑:

为了澄清我一直在寻找的理解,让我们假设您正在将具有monad的FP应用程序转换为OOP应用程序.你会怎么做把monad的职责移植到OOP应用程序?

Eri*_*ert 703

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

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

甲单子是一个的类型"放大器"的是遵循一定的规则,并具有提供某些操作.

首先,什么是"类型放大器"?我的意思是一些系统,它允许你采取一种类型,并将其变成一种更特殊的类型.例如,在C#中考虑Nullable<T>.这是一种类型的放大器.它允许你采用一种类型,比如说int,并为该类型添加一种新功能,即现在它可以在之前无法使用它.

作为第二个例子,考虑一下IEnumerable<T>.它是一种类型的放大器.它允许你采用一种类型,比如说,string并为该类型添加一种新功能,即现在可以从任意数量的单个字符串中创建一系列字符串.

什么是"特定规则"?简而言之,对于基础类型的函数有一种合理的方式来处理放大类型,使得它们遵循功能组合的正常规则.例如,如果你有一个整数函数,比如说

int M(int x) { return x + N(x * 2); }
Run Code Online (Sandbox Code Playgroud)

然后相应的函数Nullable<int>可以使那里的所有操作符和调用"以与之前相同的方式"一起工作.

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

什么是"操作"?

  1. 有一个"单元"操作(令人困惑的有时称为"返回"操作)从普通类型获取值并创建等效的monadic值.实质上,这提供了一种获取非放大类型的值并将其转换为放大类型的值的方法.它可以作为OO语言的构造函数实现.

  2. 有一个"绑定"操作,它接受一个monadic值和一个可以转换值的函数,并返回一个新的monadic值.绑定是定义monad语义的关键操作.它允许我们将非放大类型的操作转换为放大类型的操作,遵循之前提到的功能组合规则.

  3. 通常有一种方法可以将未放大的类型从放大类型中取出.严格来说,这个操作不需要monad.(虽然如果你想要一个comonad是必要的.我们不会在本文中进一步考虑这些.)

再举Nullable<T>一个例子.您可以使用构造函数将a int转换为a Nullable<int>.C#编译器为您处理最可空的"提升",但如果没有,则提升转换很简单:例如,一个操作,

int M(int x) { whatever }
Run Code Online (Sandbox Code Playgroud)

变成了

Nullable<int> M(Nullable<int> x) 
{ 
    if (x == null) 
        return null; 
    else 
        return new Nullable<int>(whatever);
}
Run Code Online (Sandbox Code Playgroud)

然后又Nullable<int>回到intValue房产.

这是关键位的功能转换.注意如何在变换中捕获可空操作的实际语义 - null传播的操作 - null.我们可以概括一下.

假设你有一个函数从intint,像我们原来的M.您可以轻松地将其转换为一个带有int和返回a 的函数,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);
}
Run Code Online (Sandbox Code Playgroud)

看看你能用它做什么?任何接受一个方法int并返回int,或接受一个int,并返回一个Nullable<int>现在可以具有施加到它的可为空的语义.

此外:假设您有两种方法

Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }
Run Code Online (Sandbox Code Playgroud)

而你想组成它们:

Nullable<int> Z(int s) { return X(Y(s)); }
Run Code Online (Sandbox Code Playgroud)

也就是说,Z是该组合物XY.但你不能这样做,因为X拿了一个int,然后Y返回一个Nullable<int>.但是由于你有"绑定"操作,你可以使这个工作:

Nullable<int> Z(int s) { return Bind(Y(s), X); }
Run Code Online (Sandbox Code Playgroud)

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;            
}
Run Code Online (Sandbox Code Playgroud)

可空的monad规则是"将两个产生nullable的函数组合在一起,检查内部函数是否为null;如果是,则生成null,如果不生成,则使用结果调用外部函数".这是可空的理想语义.

序列monad规则是"将两个生成序列的函数组合在一起,将外部函数应用于内部函数生成的每个元素,然后将所有生成的序列连接在一起".monad的基本语义在Bind/ SelectManymethods 中捕获; 这是告诉你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;            
}
Run Code Online (Sandbox Code Playgroud)

所以现在我们可以说"将这一组单个整数放大成一个整数序列.将这个特定整数转换成一串字符串,放大到一系列字符串.现在将两个操作放在一起:将这一组整数放大到串联中所有字符串序列." Monads允许您组合放大.

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

这就像问"单身人士模式解决了什么问题?",但我会试一试.

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

  • 我需要为此类型创建新功能,并仍然将此类型的旧功能组合使用以使用新功能.
  • 我需要在类型上捕获一堆操作并将这些操作表示为可组合对象,构建更大和更大的组合,直到我有恰当的操作系列表示,然后我需要开始从结果中获取结果
  • 我需要用一种讨厌副作用的语言干净地表示副作用操作

C#在其设计中使用monad.如前所述,可空图案非常类似于"可能是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关于这个主题的文章.当他们最终为我"点击"时,这篇文章向我解释了monad.
  2. 很好地说明了为什么你可能想要一个monad (在它的例子中使用Haskell).
  3. 排序,将前一篇文章"翻译"为JavaScript.

  • @slomojo:我把它改回了我写的和打算写的东西.如果你和Gabe想写下你自己的答案,你就会前进. (61认同)
  • 我更有意义地说它*增加*类型而不是*放大*它们. (39认同)
  • @Eric,当然,由你决定,但放大器意味着现有的属性被提升,这是误导. (22认同)
  • 这是一个很好的答案,但我的脑袋去了.我将在本周末跟进并盯着它,如果事情没有得到解决并且在我脑海中有意义的话,我会问你问题. (17认同)
  • 像往常一样优秀的解释Eric.对于更多理论(但仍然非常有趣)的讨论,我发现Bart De Smet关于MinLINQ的博客文章有助于将一些函数式编程结构与C#相关联.http://community.bartdesmet.net/blogs/bart/archive/2010/01/01/the-essence-of-linq-minlinq.aspx (5认同)
  • (事后补充)`M <A> - > A`没有被包含在魔法规则中,因为它不一定暴露,但我没有看到任何monad如何在没有内部功能的情况下工作.这就是我停止了大多数教程的重点. (4认同)
  • @Benjol对于某些monad来说,实际上非常重要的是`M <A> - > A`操作*不存在(例如,使用`IO` monad在Haskell中获取IO的纯模型).对于许多其他人来说,无论如何它都没有意义.你将如何实现`List <A> - > A`?列表中可能有很多As,您会选择哪个?或者确实列表可能是空的,然后你没有任何回报.但是可以实现类型为`List <A> - >(A - > List <B>) - > List <B>`*的绑定操作; 因为你可以返回一个列表而不是单个项目,你可以处理零或多个问题就好了. (4认同)
  • "(3)有一种方法可以使未放大的类型退出放大类型." - 所有Monads都不是这样 (2认同)
  • @finn,@ Eric,(3)对于提高我对monad的掌握是绝对必要的*,因为否则这些类型没有加起来(怎么可以`绑定'将`A - > M <B>`应用于` M <A>`如果它不能从'M <A>中得到`A``?) (2认同)
  • 这是有史以来最好的解释。我一直在寻找一种理解单子的方法,而您对“类型放大器”的解释终于扑朔迷离了。 (2认同)

cib*_*en1 313

为什么我们需要monad?

  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. 使用这种技术可以解决的显着问题是:

    • 具有全局状态,即函数序列中的每个函数("程序")可以共享:解决方案StateMonad.

    • 我们不喜欢"不纯的函数":为同一输入产生不同输出的函数.因此,让我们标记这些函数,使它们返回一个标记/盒装值:monad.IO

总幸福!!!!

  • 对于我作为一个来自OOP背景的人来说,这个答案真的很好地解释了拥有monad的动机以及monad实际上是什么(更多的是接受的答案).所以,我发现它非常有用.非常感谢@ cibercitizen1和+1 (27认同)
  • @DmitriZaitsev Nothing的角色可以由任何其他类型(不同的预期Real)播放.这不是重点.在该示例中,问题是当前一个函数可以将意外值类型返回到下一个函数时如何调整链中的函数,而不将后者链接(仅接受Real作为输入). (3认同)
  • 另一个混淆点是"monad"这个词在你的答案中只出现两次,并且只与其他术语 - "状态"和"IO"结合使用,没有一个以及"monad"的确切含义 (3认同)
  • @DmitriZaitsev据我所知,异常只能出现在"不纯的代码"(IO monad)中. (2认同)
  • 这句话使我感到困惑:“ ...或者相反,提取装箱的Real并用它喂`f`”?我们如何向f赋予其定义范围之外的值。为什么我们要这样做呢? (2认同)
  • @DmitriZaitsev g是R - > R - > Maybe(R).f是R - > R,而NOT Maybe(R) - > R.如何在不改变f签名及其"代码"的情况下链接它们.monad那样做.它必须将Maybe(R)中的R(如果有的话,它可能是Just(R)或Nothing)用它来输出f.当嵌套/链接函数以获得"计算"时,Monads是Haskell中重复出现问题的一种解决方案.答案的特点之一是问题,而不是全部.我认为你应该阅读"了解你的好消息"http://learnyouahaskell.com/chapters (2认同)
  • 我已经阅读了大约一年的功能性编程.这个答案,特别是前两点,最终让我理解命令式编程实际意味着什么,以及为什么函数式编程是不同的.谢谢! (2认同)
  • 如果这是中等的话我会给你50个掌声 (2认同)
  • 这里的信息非常有用,比我读过的许多其他东西和我看过的视频要有用得多。 (2认同)

Jac*_*esB 76

我会说最接近monad的OO类比是" 命令模式 ".

在命令模式中,您将普通语句或表达式包装在命令对象中.命令对象公开执行包装语句的execute方法.所以声明变成了第一类对象,可以随意传递和执行.可以组合命令,以便通过链接和嵌套命令对象来创建程序对象.

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

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

甚至更多花哨的执行语义,如事务,非确定性执行或延续,都可以用一种本身不支持它的语言来实现.如果你考虑它,这是一个非常强大的模式.

现在实际上命令模式不被用作这样的通用语言特征.将每个语句转换为单独的类的开销将导致难以忍受的样板代码量.但原则上它可以用来解决与monp用于在fp中求解相同的问题.

  • 我相信这是我见过的第一个monad解释,它不依赖于函数式编程概念,而是用真正的OOP术语表达.真的很好的答案. (14认同)
  • @DavidK.Hess 我确实非常怀疑使用 FP 来解释基本 FP 概念的答案,尤其是使用 Scala 等 FP 语言的答案。干得好,雅克 B! (4认同)
  • 这与 FP/Haskell 中 monad 的实际情况非常接近 2,除了命令对象本身“知道”它们属于哪个“调用逻辑”(并且只有兼容的可以链接在一起);调用者只提供第一个值。这不像“打印”命令可以由“非确定性执行逻辑”执行。不,它必须是“I/O 逻辑”(即 IO monad)。但除此之外,它非常接近。你甚至可以说 **Monads 只是程序**(由代码语句构建,稍后执行)。在早期,“绑定”被称为**“可编程分号”**。 (2认同)

BMe*_*eph 61

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

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

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

  • 如何注入一个值来获取该注入值类型的monadic值;
  • 如何使用在monadic值上从非monadic值生成monadic值的函数.

它解决的问题是你期望从任何界面遇到的同类问题,即"我有一堆不同的类做不同的事情,但似乎以一种具有潜在相似性的方式做那些不同的事情."我可以描述它们之间的相似性,即使这些类本身不是比"对象"类本身更接近的子类型吗?

更具体地说,Monad"接口"类似于IEnumerator或者IIterator它采用自身采用类型的类型.然而,主要的"要点" Monad是能够基于内部类型连接操作,甚至可以连接新的"内部类型",同时保持 - 甚至增强 - 主类的信息结构.


Von*_*onC 42

你有最近的演讲" Monadologie -在焦虑型专业人士的帮助 ",由克里斯托弗联赛(2010年7月12日),这是在延续和单子的话题挺有意思的.
这个(slidehare)演示文稿的视频实际上是 vimeo上提供的.
Monad部分在这一小时的视频中开始大约37分钟,并从其58幻灯片演示的幻灯片42开始.

它被表示为"功能编程的主要设计模式",但示例中使用的语言是Scala,它既是OOP又是功能.
你可以在博客文章阅读更多的单子在斯卡拉" 单子-另一种方法是在斯卡拉抽象计算 ",从Debasish戈什(2008年3月27日).

如果类型构造函数 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 }
Run Code Online (Sandbox Code Playgroud)

例如(在Scala中):

  • Option 是一个单子
    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)
      }
  • List 是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结构而构建了方便的语法:

for斯卡拉的理解:

for {
  i <- 1 to 4
  j <- 1 to i
  k <- 1 to j
} yield i*j*k
Run Code Online (Sandbox Code Playgroud)

由编译器翻译为:

(1 to 4).flatMap { i =>
  (1 to i).flatMap { j =>
    (1 to j).map { k =>
      i*j*k }}}
Run Code Online (Sandbox Code Playgroud)

关键的抽象是flatMap,通过链接绑定计算.
每次调用都flatMap返回相同的数据结构类型(但具有不同的值),作为链中下一个命令的输入.

在上面的代码片段中,flatMap将一个闭包作为输入(SomeType) => List[AnotherType]并返回一个List[AnotherType].需要注意的重要一点是,所有flatMaps都采用与输入相同的闭包类型,并返回与输出相同的类型.

这就是"绑定"计算线程的内容 - for-comprehension中序列的每个项目都必须遵守相同的类型约束.


如果你进行两次操作(可能会失败)并将结果传递给第三次,例如:

lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]
Run Code Online (Sandbox Code Playgroud)

但是如果没有利用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
  }
Run Code Online (Sandbox Code Playgroud)

而使用Monad,您可以像所有操作一样使用实际类型(Venue,User),并保持Option验证内容隐藏,所有这些都是因为for语法的平面图:

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

只有当所有三个函数都有时Some[X],才会执行yield部分; 任何None将直接返回confirm.


所以:

Monads允许在功能编程中进行有序计算,这允许我们以一种漂亮的结构化形式(有点像DSL)对动作的排序进行建模.

最强大的功能是能够将用于不同目的的monad组合成应用程序中的可扩展抽象.

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


顺便说一句,Monad不仅是FP中使用的计算模型:请参阅此博客文章.

类别理论提出了许多计算模型.其中

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

  • 我喜欢这个解释!你给出的例子很好地展示了这个概念,并且还添加了恕我直言的故事中关于SelectMany()作为Monad的恕我直言.谢谢! (2认同)

Dmi*_*sev 30

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

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

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

  • 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(从内侧).
  • Unital属性:f使用任何一方的身份函数进行组合应该会产生f.

再说一次,简单来说,我们不能只是疯狂地重新定义我们喜欢的函数组合:

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

简而言之:monad是类型扩展和组合函数的规则,它们满足两个公理 - 关联性和单位性.

实际上,您希望通过语言,编译器或框架为您实现monad,以便为您编写函数.因此,您可以专注于编写函数的逻辑,而不是担心它们的执行是如何实现的.

简而言之,这基本上就是它.


作为专业的数学家,我宁愿避免称之为h" f和" g.因为在数学上,它不是.称之为"构图"错误地认为这h是真正的数学构成,而不是.它甚至不是由f和决定的g.相反,它是我们monad新的"编写规则"功能的结果.即使后者存在,这与实际的数学成分完全不同!


Monad 不是算人!一个仿函数F是一个规则的类型去X打字F(X)和类型之间的职能(射)XY之间的功能F(X)F(Y)(范畴论发送对象对象及其态射来的态射).取而代之的是单子发送一对功能fg到一个新的h.


为了减少它的干燥,让我试着通过一个例子说明我用小部分进行注释,这样你就可以跳到正确的位置.

以Monad为例抛出异常

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

f: x -> 1 / x
g: y -> 2 * y
Run Code Online (Sandbox Code Playgroud)

但是f(0)没有定义,所以e抛出异常.那你怎么定义成分价值g(f(0))呢?当然,再次抛出异常!也许是一样的e.也许是一个新的更新例外e1.

这到底发生了什么?首先,我们需要新的异常值(不同或相同).你可以打电话给他们nothing或者null其他什么但是本质保持不变 - 他们应该是新的价值观,例如它不应该是number我们这里的例子.我不想打电话给他们null,以免混淆null任何特定语言的实施方式.同样,我宁愿避免nothing因为它经常与之相关null,原则上null应该是应该做的,然而,这个原则经常因为任何实际原因而被弯曲.

什么是例外?

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

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

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

这两个极端例外的重要区别在于,在第一种情况下没有副作用.在第二个有.这让我们得到了(千美元)的问题:

纯函数是否允许例外?

更短的答案:是的,但只有当它们不会导致副作用时.

更长的答案.要纯粹,函数的输出必须由其输入唯一确定.所以我们f通过发送0e我们称之为异常的新抽象值来修改我们的函数.我们确保该值不e包含不是由我们的输入唯一确定的外部信息,即x.所以这是一个没有副作用的异常示例:

e = {
  type: error, 
  message: 'I got error trying to divide 1 by 0'
}
Run Code Online (Sandbox Code Playgroud)

这是一个副作用:

e = {
  type: error, 
  message: 'Our committee to decide what is 1/0 is currently away'
}
Run Code Online (Sandbox Code Playgroud)

实际上,如果该消息将来可能发生变化,它只会产生副作用.但如果保证永远不会改变,那么该值就变得具有独特的可预测性,因此没有副作用.

使它更加愚蠢.返回的功能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,并g是删除0从一组定义的f.使用这组新的定义(新的更具限制性的类型x),f可以与之组合g.

但是,在编程中限制f类似的定义集是不太实际的.相反,可以使用例外.

或作为另一种方法,都像是人为制造的值NaN,undefined,null,Infinity等于是你评估1/0Infinity1/-0-Infinity.然后将新值强制回到表达式而不是抛出异常.导致您可能会或可能找不到可预测的结果:

1/0                // => Infinity
parseInt(Infinity) // => NaN
NaN < 0            // => false
false + 1          // => 1
Run Code Online (Sandbox Code Playgroud)

我们回到常规号码准备继续前进;)

JavaScript允许我们不惜任何代价继续执行数值表达式,而不会像上面的例子那样抛出错误.这意味着,它还允许组合功能.这正是monad的意思 - 编写满足本答案开头定义的公理的函数是一个规则.

但是编写函数的规则是由JavaScript处理数值错误的实现产生的,monad?

要回答这个问题,你需要的只是检查公理(左边是运动而不是问题的一部分;).

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

事实上,一个更有用的monad将成为规则,规定如果f某些人抛出异常x,那么它的组成也是如此g.另外E,只有一个可能的值(类别理论中的终端对象)使异常全局唯一.现在这两个公理可以立即检查,我们得到一个非常有用的monad.结果就是众所周知的monad.

  • 好贡献.+1但也许你会想要删除"已经找到了太长时间的解释......"是你的最长时间.其他人会根据需要判断它是否是"普通英语"的问题:"简单的英语==用简单的语言,以简单的方式". (3认同)
  • 太长且令人困惑 (2认同)
  • @seenimurugan 欢迎提出改进建议;) (2认同)

Chu*_*uck 25

monad是一种封装值的数据类型,实际上可以应用两个操作:

  • return x 创建封装的monad类型的值 x
  • m >>= f(将其读作"绑定运算符")将函数应用于fmonad中的值m

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


the*_*row 11

来自维基百科:

在函数式编程中,monad是一种用于表示计算的抽象数据类型(而不是域模型中的数据).Monads允许程序员将动作链接在一起以构建管道,其中每个动作都使用monad提供的其他处理规则进行修饰.以函数式编写的程序可以使用monad来构造包含顺序操作的过程,1 [2]或定义任意控制流(如处理并发,延续或异常).

形式上,单子通过定义两个操作(绑定和返程)和类型构造中号必须满足几个性能以允许一元函数(使用值从单子作为其参数,即函数)正确的组合物构成.返回操作从普通型取值并将其放入类型M的一个monadic容器绑定操作执行相反的过程中,提取从所述容器中的原始值,并且将它传递给相关联的下一个功能在流水线.

程序员将编写monadic函数来定义数据处理管道.单子作为一个框架,它是决定其在管道中的特定单子函数被调用的顺序可重复使用的行为,并管理全部由计算所需要的卧底工作.[3] 在每个monadic函数返回控制之后,将在管道中交错的绑定和返回运算符执行,并将处理monad处理的特定方面.

我相信它解释得很好.


Gor*_*sev 11

我将尝试使用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
}
Run Code Online (Sandbox Code Playgroud)

如果以下法律适用于所有类型T及其可能的值t

左侧身份:

CMonadic<T>.create(t).flatMap(f) == f(t)
Run Code Online (Sandbox Code Playgroud)

正确的身份

instance.flatMap(CMonadic<T>.create) == instance
Run Code Online (Sandbox Code Playgroud)

关联:

instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))
Run Code Online (Sandbox Code Playgroud)

示例:

List monad可能有:

List<int>.create(1) --> [1]
Run Code Online (Sandbox Code Playgroud)

列表[1,2,3]上的flatMap可以这样工作:

intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]
Run Code Online (Sandbox Code Playgroud)

Iterables和Observables也可以是monadic,以及Promises和Tasks.

评论:

Monads并不复杂.该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())
Run Code Online (Sandbox Code Playgroud)

事实上,这种泛型类可用作大量事物的基本模型.这(以及类别理论行话)是Monads看起来很难理解或解释的原因.它们是一个非常抽象的东西,只有在专业化后才会变得明显有用.

例如,您可以使用monadic容器对异常建模.每个容器将包含操作的结果或发生的错误.只有当前一个函数在容器中打包一个值时,才会调用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
}
Run Code Online (Sandbox Code Playgroud)


Chr*_*oph 9

快速解释:

Monad(在函数式编程中)是具有上下文相关行为的函数

上下文作为参数传递,从该 monad 的先前调用返回。它使得相同的参数在后续调用中产生不同的返回值。

等价:Monad 是实际参数取决于调用链过去的调用的函数。

典型示例:有状态函数。

常问问题

等等,“行为”是什么意思?

行为是指您针对特定输入获得的返回值和副作用。

但他们有什么特别之处呢?

在程序语义上:什么都没有。但它们仅使用纯函数进行建模。这是因为像 Haskell 这样的纯函数式编程语言只使用本身没有状态的纯函数。

但是,国家从何而来?

有状态性来自函数调用执行的顺序。它允许嵌套函数通过多个函数调用拖动某些参数。这模拟了状态。monad 只是一种软件模式,用于将这些附加参数隐藏在闪亮函数(通常称为return和 )的返回值后面bind

为什么输入/输出是 Haskell 中的 monad?

因为显示的文本是操作系统中的一种状态。如果多次读取或写入相同的文本,则每次调用后操作系统的状态将不相等。相反,您的输出设备将显示 3 倍的文本输出。为了对操作系统做出正确的反应,Haskell 需要将操作系统状态建模为一个 monad。

从技术上讲,您不需要 monad 定义。纯函数式语言可以使用“唯一类型”的概念来达到相同的目的。

非函数式语言中是否存在 monad?

是的,基本上解释器是一个复杂的单子,解释每条指令并将其映射到操作系统中的新状态。

长解释:

monad(在函数式编程中)是一种纯函数式软件模式。monad 是一个自动维护的环境(对象),可以在其中执行一系列纯函数调用。函数结果会修改该环境或与该环境交互。

换句话说,单子是一个“函数重复器”或“函数链接器”,它自动维护的环境中链接和评估参数值。通常,链接的参数值是“更新函数”,但实际上可以是任何对象(带有方法或构成容器的容器元素)。monad 是在每个评估参数之前和之后执行的“粘合代码”。这个粘合代码函数“ bind”应该将每个参数的环境输出集成到原始环境中。

因此,monad 以特定于特定 monad 的实现的方式连接所有参数的结果。控制和数据在参数之间是否流动或如何流动也是特定于实现的。

这种交织的执行允许对完整的命令式控制流(如在 GOTO 程序中)或仅使用纯函数的并行执行进行建模,而且还可以对函数调用之间的副作用、临时状态或异常处理进行建模,即使应用的函数不知道外部环境。

编辑:请注意,monad 可以以任何类型的控制流图(甚至是非确定性 NFA 方式)评估函数链,因为剩余的链是惰性评估的,并且可以在链的每个点评估多次,从而允许回溯连锁,链条。

使用 monad 概念的原因是纯功能范式,它需要一个工具以纯粹的方式模拟典型的不纯建模行为,而不是因为它们做了一些特殊的事情。

面向 OOP 人员的 Monad

在 OOP 中,单子是一个典型的对象

  • 经常调用的构造函数return值转换为环境的初始实例

  • 经常调用的可链接参数应用程序方法bind,它使用作为参数传递的函数的返回环境来维护对象的状态。

有些人还提到了第三个函数join,它是bind. 因为“参数函数”是环境中求值的,所以它们的结果嵌套在环境本身中。join是“取消嵌套”结果(展平环境)以用新环境替换环境的最后一步。

monad 可以实现 Builder 模式,但允许更通用的用途。

示例(Python)

我认为 monad 最直观的例子是 Python 中的关系运算符:

result =  0 <= x == y < 3
Run Code Online (Sandbox Code Playgroud)

您会看到它是一个 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
Run Code Online (Sandbox Code Playgroud)

真正的 monad 最多会计算每个参数一次。

现在考虑一下“结果”变量,你会得到这个链:

ret(0) .bind (lambda v: v <= x) .bind (lambda v: v == y) .bind (lambda v: v < 3)
Run Code Online (Sandbox Code Playgroud)


nom*_*men 7

单子格是否在OO中具有"自然"解释取决于单子格.在像Java这样的语言中,您可以将可能的monad转换为检查空指针的语言,以便失败的计算(即在Haskell中生成Nothing)作为结果发出空指针.您可以将状态monad转换为通过创建可变变量生成的语言以及更改其状态的方法.

monad是endofunctors类别中的monoid.

句子放在一起的信息非常深刻.你使用任何命令式语言在monad中工作.monad是一种"有序"的域特定语言.它满足了某些有趣的属性,这些属性一起使monad成为"命令式编程"的数学模型.Haskell可以轻松定义小型(或大型)命令式语言,这些语言可以以多种方式组合.

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

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

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

  • 列表(非确定性计算,通过将列表视为域)
  • 也许(计算失败,但报告不重要)
  • 错误(可能失败并需要异常处理的计算
  • Reader(可以用普通Haskell函数的组合表示的计算)
  • Writer(带有顺序"渲染"/"记录"的计算(到字符串,html等)
  • 续(续)
  • IO(依赖于底层计算机系统的计算)
  • 状态(其上下文包含可修改值的计算)

与相应的monad变换器和类型类.类型类允许通过统一其接口来组合monad的补充方法,以便具体的monad可以为monad"kind"实现标准接口.例如,模块Control.Monad.State包含一个类MonadState sm,而(State s)是一个表单的实例

instance MonadState s (State s) where
    put = ...
    get = ...
Run Code Online (Sandbox Code Playgroud)

长篇故事是monad是一个将"上下文"附加到值的仿函数,它有一种方法可以将值注入monad,并且有一种方法可以根据附加到它的上下文来评估值,至少限制的方式.

所以:

return :: a -> m a
Run Code Online (Sandbox Code Playgroud)

是一种将类型a的值注入到类型为m a的monad"action"中的函数.

(>>=) :: m a -> (a -> m b) -> m b
Run Code Online (Sandbox Code Playgroud)

是一个函数,它接受monad动作,计算结果,并将函数应用于结果.关于(>> =)的巧妙之处在于结果是在同一个monad中.换句话说,在m >> = f中,(>> =)将结果拉出m,并将其绑定到f,以便结果在monad中.(或者,我们可以说(>> =)将f拉入m并将其应用于结果.)因此,如果我们有f :: a - > mb,并且g :: b - > mc,我们可以"序列"动作:

m >>= f >>= g
Run Code Online (Sandbox Code Playgroud)

或者,使用"do notation"

do x <- m
   y <- f x
   g y
Run Code Online (Sandbox Code Playgroud)

(>>)的类型可能很有启发性.它是

(>>) :: m a -> m b -> m b
Run Code Online (Sandbox Code Playgroud)

它对应于像C这样的过程语言中的(;)运算符.它允许使用如下符号:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn
Run Code Online (Sandbox Code Playgroud)

在数学和哲学逻辑中,我们有框架和模型,它们是"自然地"建模的单体.解释是一种函数,它查看模型的域并计算命题(或公式,在一般化下)的真值(或一般化).在必要性的模态逻辑中,我们可以说如果命题在"每个可能的世界"中都是正确的话是必要的 - 如果它对于每个可接受的域都是正确的.这意味着命题语言中的模型可以被称为模型,其域包含不同模型的集合(一个对应于每个可能的世界).每个monad都有一个名为"join"的方法,它会使图层变平,这意味着每个monad动作的结果都是monad动作可以嵌入monad中.

join :: m (m a) -> m a
Run Code Online (Sandbox Code Playgroud)

更重要的是,这意味着monad在"层叠"操作下关闭.这就是monad变换器的工作原理:它们通过为类似的类型提供"类似连接"的方法来组合monad

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
Run Code Online (Sandbox Code Playgroud)

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

一个函子的自由monad是通过堆叠f生成的monad,暗示f的每个构造函数序列都是自由monad的元素(或者更准确地说,是与构造函数序列树具有相同形状的东西) 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))
Run Code Online (Sandbox Code Playgroud)

Monadism是你可以称之为"解释器"或"命令"模式的底层架构,被抽象为最清晰的形式,因为每个monadic计算必须"运行",至少是平凡的.(运行时系统为我们运行IO monad,并且是任何Haskell程序的入口点.通过按顺序运行IO操作,IO"驱动"其余计算.

连接的类型也是我们得到monad是endofunctor类别中的monoid的语句.由于其类型,加入通常对于理论目的更重要.但理解类型意味着理解monad.在函数组合的意义上,Join和monad变换器的类似连接类型是endofunctors的有效组合.把它放在类似Haskell的伪语言中,

Foo :: m(ma)< - >(m.m)a


Gul*_*han 6

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


Dav*_*ess 5

典型用法中的 Monad 与过程编程的异常处理机制功能等效。

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

然而,函数式编程语言在哲学上避免了异常处理功能,因为它们具有类似于“goto”的性质。函数式编程的观点是,函数不应该有“副作用”,比如破坏程序流程的异常。

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

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


Rob*_*Rob 5

在面向对象的术语中,monad 是一个流畅的容器。

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

可以说,如果您的 monad 类具有任何具有Something<B> work()保留类规则的签名的方法,那么它也很重要——编译器在编译时烘焙 flatMap。

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

作为一个粗略的例子,

Something<Integer> i = new Something("a")
  .flatMap(doOneThing)
  .flatMap(doAnother)
  .flatMap(toInt)
Run Code Online (Sandbox Code Playgroud)

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

在面向对象中,这可能需要一些麻烦,但是 Something 上返回 Something 的另一个子类的任何方法都满足返回原始类型容器的容器函数的标准。

这就是保留语义的方式——即容器的含义和操作不会改变,它们只是包装和增强容器内的对象。