1. 概述
作为开发者,我们经常需要对一些值进行封装,并附加一些与业务领域无关的上下文信息。比如,一个值可能需要被包装成序列、或者异步计算的结果。
这时候,Monad(单子) 就派上用场了。它是一种机制,帮助我们将这些值和附加行为组合起来。本文将通过具体例子来带你理解 Monad 的本质。
2. 什么是 Monad?
✅ Monad 不过是围绕某个值添加额外特性的计算序列机制。
这个概念源于数学中的范畴论(Category Theory),因此常常让人望而却步。但别怕,我们会用最接地气的方式把它讲清楚。
在 Scala 中,Monad 是一种将值包裹在某种上下文中,并提供操作该上下文内值的方式。比如:
Option[T]
:处理空值问题Future[T]
:处理异步操作
它们都带有一个类型参数,说明它们是对某一类型的“增强”。
2.1. 包装值到 Monad:unit 函数
每个 Monad 都必须提供一个函数,用来把普通值“装进” Monad 上下文中。我们称这个函数为 unit,也叫 pure 或者 return(不同语言叫法不同)。
在 Scala 中,通常使用伴生对象的 apply
方法来实现 unit:
object Lazy {
def apply[A](value: => A): Lazy[A] = new Lazy(value)
}
举个 🌰,我们可以用它创建一个延迟初始化的值:
val lazyInt: Lazy[Int] = Lazy {
println("The response to everything is 42")
42
}
这段代码不会立即打印任何内容,因为执行被延迟到了真正访问时。
2.2. 序列化计算:flatMap 函数
仅仅能包装值还不够,我们还需要一种方式在不拆箱的情况下对值进行变换。这就是 flatMap
的作用。
flatMap
接收一个函数,该函数从当前 Monad 内部值映射到另一个 Monad:
def flatMap[B](f: (=> A) => Lazy[B]): Lazy[B] = f(internal)
比如我们要把 lazyInt
转换成字符串:
val lazyString42: Lazy[String] = lazyInt.flatMap { intValue =>
Lazy(intValue.toString)
}
依旧不会输出任何内容,因为整个过程是惰性的。
2.3. 更贴近命令式风格:for-comprehension
为了简化链式调用,Scala 提供了 for-comprehension 语法糖。前提是你的类型实现了 map
和 flatMap
。
我们可以这样定义 map
:
def map[B](f: A => B): Lazy[B] = flatMap(x => Lazy(f(x)))
然后就可以写出类似命令式的代码:
val result: Lazy[Int] = for {
first <- Lazy(1)
second <- Lazy(2)
third <- Lazy(3)
} yield first + second + third
这段代码等价于:
val anotherResult: Lazy[Int] =
Lazy(1).flatMap { first =>
Lazy(2).flatMap { second =>
Lazy(3).map { third =>
first + second + third
}
}
}
是不是瞬间清爽多了?😊
3. Monad 三大定律
⚠️ 要成为一个合法的 Monad,光有 unit
和 flatMap
还不够,还得满足以下三条定律。
3.1. 左单位元律(Left Identity)
左单位元律表示:将一个值通过 unit
放入 Monad 后再用 flatMap
执行函数 f
,等价于直接执行 f(x)
:
Monad.unit(x).flatMap(f) = f(x)
以我们的 Lazy
为例:
Lazy(x).flatMap(f) == f(x)
代入即可验证成立。
3.2. 右单位元律(Right Identity)
右单位元律说的是:对一个 Monad 使用 flatMap
并传入 unit
,结果等于原 Monad:
x.flatMap(y => Monad.unit(y)) = x
同样适用于 Lazy
:
Lazy(x).flatMap(y => Lazy(y)) == Lazy(x)
代入可得成立。
3.3. 结合律(Associativity)
结合律最难理解,但也最重要。它表明:多个 flatMap
的调用顺序不影响最终结果:
x.flatMap(f).flatMap(g) = x.flatMap(x => f(x).flatMap(g))
对于 Lazy
来说:
Lazy(x).flatMap(f).flatMap(g) == f(x).flatMap(g)
利用左单位元律可以推导出等式成立。
✅ 满足这三条定律的类型才是真正的 Monad。
4. Bonus 方法
除了 unit
和 flatMap
外,还可以实现一些辅助方法提升可用性。
比如 flatten
,用于压平嵌套的 Monad:
def flatten(m: Lazy[Lazy[A]]): Lazy[A] = m.flatMap(x => x)
再比如 get
,用于强制提取内部值:
def get: A = internal
调用 get
会触发延迟求值并执行所有副作用。
5. 总结
在这篇文章中,我们介绍了 Scala 中的 Monad 概念:
- Monad 是一种给值加上上下文的操作模式;
- 核心方法是
unit
和flatMap
; - 必须满足左单位元、右单位元和结合律;
- Scala 中的
Option
,Future
,Either
,甚至集合类如List
,Map
等都是 Monad。
如果你还没看过这些内容,建议去 GitHub 看完整源码 👉 Baeldung/scala-tutorials