3.2 函数组合与Monad直觉


3.2 函数组合与 Monad 直觉

函数可以用 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

每个环节单一职责、独立可测——组合是函数式世界里的"特质混入"。

容器里的值:map 与 flatMap

普通函数作用于普通值。值装在容器(Option、List、Future)里时:

  • map:把函数送进容器里变换值,容器不动。Some(1).map(_ + 1)Some(2)
  • flatMap:函数本身返回容器,把两层容器压成一层。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 链——"失败即短路、成功即继续",没有一层嵌套。

flatMap 链的短路机制

亲手造一个 Monad

忘掉教科书定义,只需要两个操作加一个约定:

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] 稍后才到 不短路,并行等待

从 map 到 flatMap:一层一层拆开看

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 一通百通。

函数组合子:andThen 与 compose

val trim: String => String = _.trim val upper: String => String = _.toUpperCase val f1 = trim andThen upper // 先去空格再大写 val f2 = upper compose trim // 同样效果,方向相反的读法 f1(" scala ") // SCALA

当数据处理被表达成函数值的组合,新增一步就是往链上加一个组合子,而不是改任何已有函数。第三章的"组合"二字,最终落在把小函数当积木焊起来这件事上。

本节要点回顾

  • andThen / compose 把小函数拼成管线,替代嵌套调用。
  • map 变换箱内值,flatMap 串联会返回箱的函数并压扁
  • for 推导只是 flatMap 链的糖,跨容器统一语法。
  • Monad = pure + map + flatMap,一个 Box 二十行写完,剩下的是同一直觉的换装。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U