问题

怎样用简单的语言解释 monad?

回答
好的,我们来用最简单的方式,一步步地揭开 Monad 的神秘面纱。请你想象一下,我们现在在一个非常有条理、但又有点特别的商店里。

第一步:盒子(或者说“包裹”)

想象一下,你买了一个很漂亮的礼物,但是这个礼物被小心翼翼地放进了一个盒子里。这个盒子不仅仅是一个普通的盒子,它有一个特别的能力:

盒子可以包含东西: 你可以把苹果放进盒子,也可以把香蕉放进盒子。
盒子本身就有一些限制: 你不能直接咬盒子里面的苹果,你得先把盒子打开。

在编程里,这个“盒子”就像是你的数据被包装起来的一种方式。比如,在一个列表中,你有很多苹果,或者一个对象,它被包装在一个 `Maybe` 类型里(表示可能存在也可能不存在的值)。

我们给这个盒子起个名字,叫做“容器”。

第二步:给盒子里的东西做点事——“映射”(map)

现在你有了装了苹果的盒子。你想给这个苹果削皮。但你不能直接削盒子里的苹果,你得先打开盒子,把苹果拿出来,削完皮,然后再放进一个新的盒子里。

而且,你希望这是一个“智能”的削皮操作。也就是说,你有一个方法:

`map(函数)`:这个函数能接收盒子里的东西,然后对它做一些改变,最后把改变后的东西放回一个新盒子里。

想象一下,你有一个函数叫做 `削皮`。

你可以对装了苹果的盒子调用 `map(削皮)`。
`map` 会悄悄地打开盒子,拿到苹果,用 `削皮` 函数处理它,然后把削好皮的苹果放进一个新的盒子里给你。

关键点: `map` 操作总是会产生一个新的、包含新内容的盒子。它不会改变原来的盒子。

第三步:连接多个操作——“绑定”(bind / flatMap)

现在我们遇到了一个稍微复杂点的情况。你想给苹果削皮,然后削完皮的苹果,你还想把它切成两半。

如果我们继续用 `map` 的方式,可能会变成这样:

1. 你有一个装了苹果的盒子 A。
2. 你用 `map(削皮)`,得到一个装了削好皮的苹果的盒子 B。
3. 你再用 `map(切成两半)`,得到一个装了切好一半的苹果的盒子 C。

看起来没问题。但是,如果中间某个步骤失败了怎么办?

比如,我们现在考虑的是一个“可能为空”的盒子。有时候盒子里是苹果,有时候盒子里是空的(比如你买的礼物是个空盒子)。

如果盒子 A 是空的,`map(削皮)` 会得到一个空盒子。
如果盒子 B 是空的,`map(切成两半)` 会得到一个空盒子。

这很好,`map` 能够处理这种“空”的情况,并保持传递下去。

但是,现在我们有一个更棘手的问题。

设想一下,你的“削皮”函数,它本身不是直接给你一个削好皮的苹果,而是给你一个新的盒子,里面装着削好皮的苹果。

也就是说,我们现在有一个函数叫做 `削皮_并_放回新盒子(苹果)`。它会返回一个“装了削好皮的苹果的盒子”。

如果我们对“装了苹果的盒子 A”使用 `map(削皮_并_放回新盒子)`,会发生什么?

`map` 会打开盒子 A,拿到苹果。
它会调用 `削皮_并_放回新盒子(苹果)`。
这个函数返回了一个“装了削好皮的苹果的盒子”(我们称它为盒子 X)。
`map` 会把这个盒子 X 直接返回给你。

这样看起来好像也没错。但是,如果你仔细想想,你得到了一个“盒子里的盒子”!就像你有一个装苹果的盒子,里面又有一个装削好皮的苹果的盒子。这可不是我们想要的。我们想要的是一个平坦的、直接装了削好皮的苹果的盒子。

这就轮到 Monad 的核心——“绑定”(bind)登场了!

“绑定”操作 (`bind` 或 `flatMap`) 做的事情是:

它接收一个“容器”。
它接收一个函数,这个函数接受容器里的内容,并返回一个新的容器。
重点来了: `bind` 会把这个新的容器“摊平”,或者说“解包”,让最终返回的结果,依然是一个单层的容器。它会处理“盒子里的盒子”这种嵌套情况,直接返回那个最里面的盒子。

所以,如果用 `bind` 来处理 `削皮_并_放回新盒子` 这个函数:

1. 你有一个装了苹果的盒子 A。
2. 你对盒子 A 调用 `bind(削皮_并_放回新盒子)`。
3. `bind` 打开盒子 A,拿到苹果。
4. 它调用 `削皮_并_放回新盒子(苹果)`,这个函数返回了盒子 X(里面装着削好皮的苹果)。
5. 因为是 `bind`,它不会把盒子 X 再放进另一个盒子,而是直接返回盒子 X。

这样,你就得到了一个平坦的、装了削好皮的苹果的盒子,而不是“盒子里的盒子”。

Monad 的构成:一个“容器”加上“map”和“bind”的能力

到这里,我们就基本理解了 Monad 的核心概念。一个 Monad 是一种特殊的“容器”,它提供了:

1. 包裹值的能力 (return / pure): 就像把一个值放进一个默认的、空的容器里。
2. 映射能力 (map): 能够将一个函数应用到容器里的值上,并返回一个新的容器。
3. 绑定能力 (bind / flatMap): 能够将一个返回容器的函数应用到容器里的值上,并能处理嵌套,保持容器的层级不变。

为什么需要 Monad?它的好处是什么?

Monad 之所以重要,是因为它提供了一种统一的方式来处理那些“有上下文”的数据。这些“上下文”可以有很多种:

可能不存在的值 (`Maybe` / `Optional`): 用 Monad 可以优雅地处理 `null` 或 `undefined`,避免繁琐的 `if` 判断。
异步操作 (`Promise` / `Future`): `Promise` 的 `.then()` 就是一个典型的 Monad 的 `bind` 操作,它允许你链式处理一系列异步操作,而不用写难以理解的回调地狱。
错误处理 (`Either` / `Result`): 可以方便地传递成功的值,或者在链式操作中传递错误信息。
列表/集合 (`List` / `Array`): `flatMap` 在处理列表时,可以将列表中的每个元素都映射到一个新的列表,然后将这些新的列表合并成一个单一的列表。
状态管理 (`State`): 可以让你在不显式传递状态的情况下,安全地修改和传递状态。
I/O 操作 (`IO`): 封装了可能引起副作用的操作,让代码更可控。

用一个更生动的比喻:传送带系统

想象你有一个复杂的工厂,有很多操作步骤。

物品 (Value): 你的产品,比如一个苹果。
传送带 (Container/Monad): 物品在工厂里流转的方式,它有自己的规矩。
第一个操作员 (map): 他可以拿到传送带上的苹果,给它削皮,然后把削好皮的苹果放回另一条新的传送带上。他总是产生一条新传送带。
第二个操作员 (bind/flatMap): 这个操作员很特别。他拿到的苹果,他不是直接处理,而是会用一个机器,这个机器会处理苹果,然后产生一条全新的传送带,上面是处理好的苹果。如果他直接把这个新传送带交给你的话,你就会得到“传送带上的传送带”。但是,他会把这个新传送带“压扁”,把里面的苹果直接放到一条平整的传送带上。

为什么 Monad 是“序列化”或“链式”操作的关键?

Monad 的 `bind` 操作允许你把一系列独立的、接受一个“容器里的值”并返回“一个新的容器”的函数,像链条一样连接起来。

比如,我们有:
1. `削皮(苹果)` > 返回 `装了削好皮的苹果的盒子`
2. `切块(苹果)` > 返回 `装了切块的苹果的盒子`

如果你想先削皮,再切块,你不需要自己去管理盒子的打开和关闭,也不需要担心中间环节的值是否有效(比如是否是空盒子)。Monad 的 `bind` 会帮你把这些步骤无缝地串联起来,并且正确地处理好每一个环节的“上下文”(比如空值、错误等)。

Monad 的两个核心定律 (简单理解)

为了让 Monad 行为一致,有一些约定俗成的规则,最重要的两个是:

1. 左单位律 (Left Identity):
先把值包装起来,再进行 `bind` 操作,结果应该和直接对值进行 `bind` 操作一样。
就像:`bind(return(x), f)` 应该等于 `f(x)`。
简单说:先包装再处理,应该等同于直接处理。

2. 右单位律 (Right Identity):
用 `bind` 操作一个已经包装好的值,然后用 `return` 再包装一次,结果应该和原先包装好的值一样。
就像:`bind(m, return)` 应该等于 `m`。
简单说:用“处理并重新包装”的方法处理一个值,最后再包装一次,应该回到原样。

这两个定律保证了 Monad 的组合是可预测的。

总结一下:

Monad 是一种用于处理“有上下文”的值的模式。它提供了一种统一的方式来包装值,并对这些包装后的值进行一系列的操作,而无需暴露底层上下文的具体实现细节。`map` 用于简单的转换,`bind` (或 `flatMap`) 则用于更强大的转换,特别是当转换函数本身也会返回一个新的 Monad 时,`bind` 可以避免嵌套,保持结果的扁平化。

这就像一个非常有条理的邮局系统。你寄一个包裹(包装值),邮局有一套方法来处理包裹(`map`),如果某个处理步骤需要再寄一个新包裹(返回新 Monad),邮局的“转寄”服务(`bind`)会确保这些包裹层层嵌套后,你最终收到的还是那个最里面的、处理好的物品,而不是一堆层层包裹的盒子。

希望这个详细的解释能帮助你理解 Monad 的核心思想!

网友意见

user avatar

数学的定义其它答案都解释得很多了。这里,我提供一种从具体例子到抽象的解释。希望能帮 monad 除掉「the m-word」的名声。

1 为什么说列表是 Monad?

问题标签里有 Scala,这里我们就先来考察 Scala 里的 Seq 是否是 monad。

我们来看看 Seq 能干什么:

首先,我们可以用一个元素构造 Seq:

       val persons = Seq(john)     

注意这里的构造指:根据一个元素构造出仅含一个元素的 Seq。

第二,我们还可以在一个 Seq 对象上 flatMap(这里顺便提供一个

关于 flatMap 的直观解释

       persons.flatMap(p => p.favoriteBooks)     

Monad 就是对这两个行为的抽象。我们分别称呼上面两个函数为 idflatMap(这里我们不使用 return 和 bind 这两个不直观的名字)。

2  Monad 有什么好?

这样的抽象到底有什么好处?Monad 在数学上的优美这里不重复,请参考其它答案。这里只讲与程序员最相关的一个优点。在一些编程语言里,比如 Scala,Monad 是有专用语法糖的。我们以一个实际例子来说明:

假设你有一个语料库,它由不同的文档(类型为 Document)构成,每篇 Document 又有若干句子(类型为 Sentence),每个句子又由词构成(类型为 Word)。即,语料是由 Seq[Document] 组成,Document 里的 sentences 方法返回一个 Seq[Sentence],Sentence 里的 words 方法返回 Seq[Word]。现在你需要获取语料中每个长度大于 4 个字母的词的首字母。

传统的 for 嵌套写法这里就不提了。普遍的写法是利用 flatMap 和 map:

       val lengths =    documents.flatMap(d =>      d.sentences.flatMap(s => s.words.filter(w => w.length > 4)))            .map(w => w(0))     

然而这看着依然很乱。有没有更优雅的写法?有!Scala 语言里可以这样:

       for {   d <- documents   s <- d.sentences   w <- s.words   if w.length > 4 } yield w(0)     

凡是定义有 flatMap 和 map 的类,都可以在 Scala 里这么写。这不是一件激动人心的事情吗?

3 到底 Monad 是什么?

我们可以通过类比来试着猜一下,一个 monad 的 id 和 flatMap 分别具有怎样的参数和返回值。

在 Seq 的例子中,id 就是根据一个元素构造出 Seq 的函数,即

       def id[X](x: X): Seq[X] = Seq(x)     

如果我们把 Seq 的 flatMap 改写成一个全局函数,它会是这样

       def flatMap[X, Y](xs: Seq[X], f: X => Seq[Y]): Seq[Y] = xs.flatMap(f)     

好,现在我们来抽象一下这两个函数。假设现在有一种抽象的、行为类似 Seq 的类型,叫 M。在它上面我们可以类比 Seq,定义出抽象的 id 和 flatMap:

       def id[X](x: X): M[X] def flatMap[X, Y](xs: M[X], f: X => M[Y]): M[Y]      

看,与具体的 Seq 的 id 和 flatMap 没有太大区别。事实上,列表就是最天然的 monad.

有了这两个函数的确切签名,我们就可以给出 monad 的定义了:

能定义出上面两个函数的类型 M 就是 monad。

当然,这两个函数还必须满足一些 monad 公理 <del>这里暂不关心</del>(见文末更新)。然而类型系统无法验证这些性质,实际使用中都是程序员自觉。

具体到代码上,我们可以用下面这个 typeclass 来定义:

       trait Monad[M[_]] {   def id[X](x: X): M[X]   def flatMap[X, Y](xs: M[X], f: X => M[Y]): M[Y] }     

4  还有哪些 Monad 的实例?

有了这个定义,我们会瞬间发现,其实 Monad 无处不在。除了像 Seq 那样的列表类型,这里再多考察几个其它类型的。

4.1  Option Monad

我们来看看许多函数式语言里都有的 Option 是否是 monad。方便起见,这里仍然用 Scala 语言。我们试试能不能定义出 Option 的 id 和 flatMap:

       def id[X](x: X): Option[X] = Some(x) def flatMap[X, Y](ox: Option[X], f: X => Option[Y]): Option[Y] =   os.flatMap(f)     

看,我们成功定义出了这两个函数。所以 Option 是 monad。我们说明这个这又有什么用?

来,我们看看,究竟什么时候需要 Option?有返回 null 的需求,但不想看到 NullPointerException 时。比如 Java 代码中经常出现下面这种代码:

       if (person.bestFriend != null) {   if (person.bestFriend.favoriteBook != null) {     return /* anything */   }   else return null } else return null     

为了拯救这种代码,我们需要引入 Option。本来 person.bestFriend 返回的要么是一个 Person 对象,要么是一个邪恶的 null。我们可以将其返回类型改为 Option[Person]。关于 Option 是什么,请参考任意有 Option 的标准库文档。

有了 Option 是 monad 的证据,我们就可以写出如下的代码了:

       for {   bf <- person.bestFriend   fb <- bf.favoriteBook } yield /* whatever */      

看,既不会导致运行时 NullPointer 错误,又不用手动检查 null,代码又易读。

4.2  分布 Monad

为了深化对 monad 的理解,我们再来丧病地考察分布(Distribution),看看它是不是个 monad。

我们明确一下 Distribution 到底是个什么东西。这里用 Scala 的 trait 定义:

       trait Distribution[+X] { outer =>   def sample: X   def flatMap[Y](f: X => Distribution[Y]): Distribution[Y] =      new Distribution[Y] {       def sample: Y = f(outer.sample).sample     } }      

有了这个定义,我们很容易就能写出 id 和 flatMap 的 Distribution 版本:

我们先写出 flatMap。直接使用 Distribution 里的 flatMap 即可:

       def flatMap[X, Y](xs: Distribution[X], f: X => Distribution[Y]) =    xs.flatMap(f)      

然后是 id。函数 id 的作用就是:接收一个样本,构造出一个分布。那么一个样本能构造出来什么分布呢?答案就是抽样时永远返回同一个样本的分布:

       def id[X](x: X): Distribution[X] = new Distribution {   def sample: T = x }     

因此,我们意识到,分布也是 monad!

我们来定义两个常用的分布:Uniform 和 Gaussian:

       case class Uniform(a: Double, b: Double) extends Distribution[Double] {   def sample = scala.util.Random.nextDouble() * (b - a) + a }  case class Gaussian(μ: Double, σ2: Double) extends Distribution[Double] {   def sample = scala.util.Random.nextGaussian() * math.sqrt(σ2) + μ }     

哈哈,展示 monad 的威力的时候到了。我们可以非常自然地用现有分布定义一个新分布:

       val d = for {   x <- Uniform(0, 2)   y <- Gaussian(x, 1) } yield x + y      

就像写数学公式一样自然。

Monad 为我们带来了类型安全,能让我们的程序易读。请不要以「the m-word」称呼它。

------- UPDATE --------

还是把 Monad 对 id 和 flatMap 的要求(即 Monad 公理)写出来吧:

第一条:id 是 flatMap 的左单位元

       flatMap(id(x), f) 等于 f(x)     

第二条:id 是 flatMap 的右单位元

       flatMap(xs, id) 等于 xs     

第三条:结合律

       flatMap(flatMap(m, f), g) 等于 flatMap(m, x => flatMap(f(x), g))     

当然,写成面向对象的形式可能更好理解:

       id(x).flatMap(f)         等于 f(x) xs.flatMap(id)           等于 xs xs.flatMap(f).flatMap(g) 等于 xs.flatMap(x => f(x).flatMap(g))     

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有