函数可以用 andThen、compose 串成管线;装进容器的值用 map、flatMap 变换与串联。Monad 不是玄学,它就是"实现了这两个操作且遵守两条结合律的容器"——本节你将亲手写一个。
val trim: String => String = _.trim val toInt: String => Option[Int] = s => s.toIntOption val parse: String => Option[Int] = trim andThen toInt parse(" 42 ") // Some(42)
f andThen g 先 f 后 g;g compose f 顺序相反。当管线变长,这种"小函数拼大函数"的风格比嵌套调用可读得多:
val pipeline = readLines andThen filterComment andThen parseConfig andThen validate
每个环节单一职责、独立可测——组合是函数式世界里的"特质混入"。
普通函数作用于普通值。值装在容器(Option、List、Future)里时:
Some(1).map(_ + 1) 得 Some(2)。Some(1).flatMap(n => Some(n+1)) 得 Some(2),而不是 Some(Some(2))。def parseId(raw: String): Option[Int] = raw.toIntOption def findUser(id: Int): Option[User] = db.find(id) def userEmail(u: User): Option[String] = u.email // 嵌套 if-null 的函数式替代 def emailOf(raw: String): Option[String] = parseId(raw).flatMap(findUser).flatMap(userEmail) // for 推导是 flatMap 链的语法糖 def emailOf2(raw: String): Option[String] = for id <- parseId(raw) user <- findUser(id) mail <- userEmail(user) yield mail
for 推导展开后就是上面的 flatMap 链——"失败即短路、成功即继续",没有一层嵌套。
忘掉教科书定义,只需要两个操作加一个约定:
enum Box[+A]: case Full(value: A) case Empty object Box: def pure[A](a: A): Box[A] = Full(a) extension [A](b: Box[A]) def map[B](f: A => B): Box[B] = b match case Full(v) => Full(f(v)) case Empty => Empty def flatMap[B](f: A => Box[B]): Box[B] = b match case Full(v) => f(v) case Empty => Empty
pure 把值装箱,map 箱内变换,flatMap 箱间串联且空箱短路。任何满足这三件套的容器都是 Monad:Option(可能没有)、List(可能有多个)、Future(现在还没有)、Either(可能出错)——同一个 flatMap 直觉全部通用。以后再看到"Monad 是自函子范畴上的幺半群",你知道它只是在数学化你已经写过的这 20 行代码。
标准库里现成的 Monad 语境:
| 容器 | 语境 | flatMap 短路条件 |
|---|---|---|
| Option[A] | 可能缺席 | None |
| Either[E, A] | 可能失败 | Left |
| List[A] | 多个可能 | 空列表 |
| Future[A] | 稍后才到 | 不短路,并行等待 |
Option 的嵌套是 Monad 直觉的最佳入口。假设两步查询都可能失败:
def findUser(id: Long): Option[String] = Some("alice") def findDept(u: String): Option[String] = Some("platform") // 笨写法:map 套 map,得到 Some(Some("platform")) val nested = findUser(1L).map(u => findDept(u)) // flatMap:把外层与内层的"上下文"压成一层 val flat = findUser(1L).flatMap(u => findDept(u)) // flat: Option[String] = Some(platform) // for 推导是 flatMap 链的语法糖,语义完全等价 val sweet = for u <- findUser(1L) d <- findDept(u) yield d
三段代码逐层演化的读法:map 保持层数、flatMap 压平层数、for 把 flatMap 链翻译成命令式外观。所谓 Monad,就是"定义了 flatMap 与单位函数、并满足结合律的上下文"——Option 的上下文是"可能没有",Future 的是"还没有",List 的是"有多个"。掌握 flatMap,六种 Monad 一通百通。
val trim: String => String = _.trim val upper: String => String = _.toUpperCase val f1 = trim andThen upper // 先去空格再大写 val f2 = upper compose trim // 同样效果,方向相反的读法 f1(" scala ") // SCALA
当数据处理被表达成函数值的组合,新增一步就是往链上加一个组合子,而不是改任何已有函数。第三章的"组合"二字,最终落在把小函数当积木焊起来这件事上。