833

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

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

更新

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

4

20 回答 20

803

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

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

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

首先,什么是“类型放大器”?我的意思是一些系统,它可以让你把一个类型变成一个更特殊的类型。例如,在 C# 中考虑Nullable<T>. 这是一种类型的放大器。它允许您获取一个类型,例如int,并为该类型添加一个新功能,即现在它可以为 null 而以前不能。

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

什么是“某些规则”?简而言之,底层类型上的函数有一种合理的方式可以在放大类型上工作,从而它们遵循函数组合的正常规则。例如,如果你有一个整数函数,比如说

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

那么相应的函数就Nullable<int>可以使那里的所有运算符和调用“以与以前相同的方式”一起工作。

(这是非常模糊和不精确的;您要求的解释没有假设任何关于功能组合的知识。)

什么是“操作”?

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

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

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

再次,Nullable<T>举个例子。您可以使用构造函数将 aint转换为 a 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);
}

将 a Nullable<int>back 变成 anint是使用该Value属性完成的。

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

假设你有一个 from intto函数int,就像我们原来的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);
}

看看你能用它做什么?任何采用 anint并返回 anint或采用 anint并返回 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 上的绑定操作是使放大类型上的函数组合起作用的原因。我在上面提到的“规则”是 monad 保留了正常函数组合的规则;与恒等函数组合产生原始函数,组合是关联的,等等。

在 C# 中,“绑定”称为“SelectMany”。看看它是如何在序列单子上工作的。我们需要做两件事:将值转换为序列和对序列进行绑定操作。作为奖励,我们还有“将序列转回值”。这些操作是:

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

可空单子规则是“将两个产生可空值的函数组合在一起,检查内部函数是否产生空值;如果是,则产生空值,如果不是,则使用结果调用外部函数”。这就是 nullable 的期望语义。

序列单子规则是“将两个产生序列的函数组合在一起,将外部函数应用于内部函数产生的每个元素,然后将所有结果序列连接在一起”。monad 的基本语义在Bind/SelectMany方法中被捕获;这是告诉你 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 允许你组成你的放大。

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

这更像是在问“单例模式解决了哪些问题?”,但我会试一试。

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

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

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

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

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

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


建议,进一步阅读:

  1. 对于 C# 中单子的更深入和理论上合理的解释,我强烈推荐我 ( Eric Lippert的) 同事 Wes Dyer 关于该主题的文章。这篇文章是当他们最终为我“点击”时向我解释的单子。
  2. 很好地说明了为什么您可能需要一个 monad (在其示例中使用 Haskell)
  3. 某种意义上,将上一篇文章“翻译”成 JavaScript。

于 2010-04-24T14:28:51.360 回答
406

为什么我们需要单子?

  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(function from two reals into a real),让我们允许g : Real,Real -> Real | Nothing(function from two reals into (real or nothing))。

  4. 但是函数应该(更简单)只返回一件事

    解决方案:让我们创建一种要返回的新数据类型,一种“装箱类型”,它可能包含一个真实的或只是什么都没有。因此,我们可以拥有g : Real,Real -> Maybe Real. 好的但是 ...

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

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

    在我们的例子中:( g >>= f连接/组合gf)。我们想要>>=获取g' 的输出,检查它,以防万一它Nothing只是不调用f并返回Nothing; 或者相反,取出盒装Real并用它喂食f。(这个算法只是类型的实现>>=Maybe

  6. 出现了许多其他问题,可以使用相同的模式解决: 1. 使用“盒子”来编码/存储不同的含义/值,并让类似g的函数返回那些“装箱值”。2. 让作曲家/链接器g >>= f帮助将g' 的输出连接到f' 的输入,所以我们根本不需要改变f

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

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

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

总幸福!!!!

于 2015-01-25T10:42:25.057 回答
93

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

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

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

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

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

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

于 2010-05-19T17:58:38.197 回答
65

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

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

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

  • 如何注入一个值以获取该注入值类型的一元值;
  • 如何在单子值上使用从非单子值生成单子值的函数。

它解决的问题与您期望从任何接口中得到的问题类型相同,即,“我有一堆不同的类,它们做不同的事情,但似乎以具有潜在相似性的方式做这些不同的事情。如何我能描述它们之间的相似性吗,即使这些类本身并不是比“对象”类本身更接近的任何东西的子类型?”

更具体地说,Monad“接口”类似于IEnumeratorIIterator在于它采用本身采用类型的类型。虽然主要的“点”Monad是能够连接基于内部类型的操作,甚至可以拥有一个新的“内部类型”,同时保持 - 甚至增强 - 主类的信息结构。

于 2010-07-17T00:14:57.863 回答
42

您最近有一个由Christopher League(2010 年 7 月 12 日)发表的“ Monadologie - 类型焦虑的专业帮助”,它对延续和 monad 的主题非常有趣。 此(幻灯片共享)演示文稿的视频实际上可在vimeo 获得。 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 unit[A] (x: A): Option[A] = Some(x)

    def flatMap[A,B](m:Option[A])(f:A =>Option[B]): Option[B] =
      米匹配{
       案例无 => 无
       案例一些(x)=> f(x)
      }
  • List是单子
    def unit[A] (x: A): List[A] = List(x)

    def flatMap[A,B](m:List[A])(f:A =>List[B]): List[B] =
      米匹配{
        案例零 => 零
        案例 x::xs => f(x) ::: flatMap(xs)(f)
      }

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

forScala 中的理解

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,您可以像所有操作一样使用实际类型(Venue, User),并隐藏选项验证内容,这一切都是因为 for 语法的平面图:

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

Some[X]仅当所有三个函数都具有;时才会执行 yield 部分。anyNone将直接返回到confirm.


所以:

Monads 允许在函数式编程中进行有序计算,这使我们能够以良好的结构化形式对动作序列进行建模,有点像 DSL。

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

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


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

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

  • 计算的阿罗模型
  • Monad 计算模型
  • 计算的应用模型
于 2010-07-17T22:39:32.553 回答
41

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

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

monad (在计算机科学中)形式上是一个映射,它:

  • X将某种给定编程语言的每一种类型发送到一个新类型T(X)(称为“T值在”中的“计算类型X”);

  • 配备规则,用于组合表单 f:X->T(Y)和函数的两个g:Y->T(Z)函数g∘f:X->T(Z)

  • 以一种明显意义上的关联方式,并且相对于给定的单位函数称为pure_X:X->T(X),被认为是对简单地返回该值的纯计算取一个值。

所以简单来说,monad从任何类型传递X到另一种类型T(X)规则,也是从两个函数f:X->T(Y)g:Y->T(Z)(你想组合但不能)传递到新函数的h:X->T(Z)规则。然而,这不是严格数学意义上的组合。我们基本上是在“弯曲”函数的组合或重新定义函数的组合方式。

另外,我们需要 monad 的组合规则来满足“显而易见的”数学公理:

  • 关联性:fg然后用h(从外部)组合应该g与用h然后用f(从内部)组合。
  • 单一属性:与任一侧ff等函数组合应该产生。

同样,简单来说,我们不能疯狂地按照我们的喜好重新定义我们的函数组合:

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

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

实际上,您希望 monad 由为您负责组合函数的语言、编译器或框架为您实现。因此,您可以专注于编写函数的逻辑,而不必担心它们的执行方式。

简而言之,基本上就是这样。


作为专业数学家,我更愿意避免称和h的“组成” 。因为在数学上,它不是。将其称为“组合”错误地假定这是真正的数学组合,但事实并非如此。它甚至不是由和唯一确定的。相反,它是我们的 monad 新的“组合规则”函数的结果。即使后者存在,它也可能与实际的数学组成完全不同!fghfg


为了让它不那么枯燥,让我试着用我用小部分注释的例子来说明它,所以你可以直接跳到重点。

异常抛出作为 Monad 示例

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

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

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

这里究竟发生了什么?首先,我们需要新的异常值(不同或相同)。您可以调用它们nothing或其他名称,但本质保持不变——它们应该是新值,例如,在我们的示例中null它不应该是 a 。number我不想调用它们null以避免与如何null用任何特定语言实现混淆。同样,我更喜欢避免nothing,因为它通常与 相关联null,原则上,这是null应该做的,但是,无论出于何种实际原因,该原则经常被弯曲。

究竟什么是异常?

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

Exception 是一个对象,它封装了有关无效执行结果如何发生的信息。

这可以包括丢弃任何细节并返回单个全局值(如NaNor null)或生成长日志列表或究竟发生了什么,将其发送到数据库并在整个分布式数据存储层进行复制;)

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

纯函数中是否允许异常?

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

更长的答案。为了纯粹,您的函数的输出必须由其输入唯一确定。f所以我们通过发送到我们称之为异常0的新抽象值来修改我们的函数。e我们确保该值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'
}

实际上,如果该消息将来可能发生变化,它只会产生副作用。但是,如果保证永远不会改变,那么该值将变得唯一可预测,因此没有副作用。

让它变得更加愚蠢。一个返回42ever 的函数显然是纯粹的。但是,如果某个疯狂的人决定创建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删除。有了新的定义集(新的限制性更强的类型),就可以与.0fxfg

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

或者作为另一种方法,创建人工值,如, ,NaNundefined。因此您评估to和to 。然后将新值强制返回到您的表达式中,而不是抛出异常。导致您可能会或可能无法预测的结果:nullInfinity1/0Infinity1/-0-Infinity

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

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

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

但是,由 JavaScript 处理数字错误的实现产生的组合函数规则是 monad 吗?

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

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

实际上,一个更有用的 monad 将改为规定 iff为 some 抛出异常的规则x,它与 any 的组合也是如此g。另外,使异常E全局唯一,只有一个可能的值(类别理论中的终端对象)。现在这两个公理可以立即检查,我们得到了一个非常有用的单子。结果就是众所周知的Maybe monad

于 2016-05-20T11:17:35.447 回答
31

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

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

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

于 2010-04-24T19:37:42.543 回答
17

来自维基百科

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

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

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

我相信它很好地解释了它。

于 2010-04-24T13:45:53.333 回答
13

我将尝试使用 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 也一样。

评论

单子并不复杂。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
}
于 2013-12-26T18:35:07.827 回答
7

单子在 OO 中是否具有“自然”解释取决于单子。在像Java 这样的语言中,您可以将maybe monad 翻译成检查空指针的语言,以便失败的计算(即,在Haskell 中产生Nothing)发出空指针作为结果。您可以将状态单子翻译成通过创建可变变量和方法来更改其状态而生成的语言。

单子是内函子类别中的一个幺半群。

句子放在一起的信息很深。并且您使用任何命令式语言在 monad 中工作。monad 是一种“序列化”的领域特定语言。它满足某些有趣的特性,这些特性共同使 monad 成为“命令式编程”的数学模型。Haskell 使定义小型(或大型)命令式语言变得容易,它们可以以多种方式组合。

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

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

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

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

带有相应的单子转换器和类型类。类型类允许通过统一它们的接口来组合 monad 的补充方法,以便具体的 monad 可以实现 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 类型的单子“动作”的函数。

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

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

m >>= f >>= g

或者,使用“do notation”

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在“层堆叠”操作下是封闭的。这就是 monad 转换器的工作方式:它们通过为以下类型提供“类似连接”的方法来组合 monad

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

这样我们就可以将 (MaybeT m) 中的动作转换为 m 中的动作,从而有效地折叠层。在这种情况下,runMaybeT :: MaybeT ma -> m (Maybe a) 是我们的类连接方法。(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 Transformer 的 join-like 类型是内函子的有效组合。用类似 Haskell 的伪语言来说,

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

于 2012-12-01T03:58:57.603 回答
4

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

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

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

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

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

于 2012-12-23T17:53:46.133 回答
4

在 OO 术语中,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)

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

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

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

于 2016-11-29T21:33:05.360 回答
3

monad 是一组函数

(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)
于 2014-02-16T16:11:38.350 回答
3

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

于 2019-07-21T10:34:55.187 回答
2

如果您曾经使用过 Powershell,那么 Eric 描述的模式应该听起来很熟悉。 Powershell cmdlet是单子;功能组合由管道表示。

Jeffrey Snover 对 Erik Meijer 的采访更详细。

于 2010-04-25T15:18:42.520 回答
2

一个简单的 Monads 解释和 Marvel 的案例研究在这里

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

  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]的"return" ,也为该特定F定义。要成为 Monad,F必须为其定义这两个函数。

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

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

但是给定的F也可以定义自己的不透明“内置”特殊函数,这些函数是用户无法自己定义的(在语言中),比如

  • “随机”(范围 => 随机[Int]
  • “打印”(字符串 => IO[()]
  • “尝试……抓住”等。
于 2019-04-21T14:11:18.087 回答
1

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

这在涉及副作用时尤其重要,即当某些状态发生变化时(如果 f1、f2、f3 没有副作用,则它们的评估顺序无关紧要;这是 pure函数式语言,例如能够并行化这些计算)。纯函数越多越好。

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

这只是一方面,正如这里警告的那样。

于 2013-04-05T15:17:23.997 回答
1

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

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

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

这个 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;
}
于 2015-03-08T09:43:14.440 回答
1

快速解释:

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

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

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

典型示例:有状态函数。

常问问题

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

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

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

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

但是,状态从何而来?

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

为什么输入/输出在 Haskell 中是一个单子?

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

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

单子是否存在于非函数式语言中?

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

长解释:

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

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

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

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

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

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

面向 OOP 人员的 Monad

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

  • 一个经常被调用的构造函数return,它将一个值转换为环境的初始实例

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

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

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

示例(Python)

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

result =  0 <= x == y < 3

您会看到它是一个 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

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

现在想想“结果”变量,你会得到这个链:

ret(0) .bind (lambda v: v <= x) .bind (lambda v: v == y) .bind (lambda v: v < 3)
于 2021-07-05T20:34:26.563 回答
1

我能想到的最简单的解释是,monad 是一种用修饰结果组合函数的方式(又名 Kleisli 组合)。“修饰”函数具有签名a -> (b, smth)whereabare 类型(认为Int, Bool)可能彼此不同,但不一定 - 并且smth是“上下文”或“修饰”。

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

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

所以,如果我们说我们有一个(Int, String)表示 s 上的计算结果的元组类型Int,它也记录他们的动作,(_, String)作为“装饰” - 动作的日志 - 和两个函数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.")

从这个 Kleisli 组合函数可以推导出通常的一元函数。

于 2019-11-10T09:24:57.807 回答