从 JVM 语言到函数式范式
Scala 是一门站在 JVM 上的多范式语言:既能像 Java 一样写面向对象,也能流畅地写函数式代码。它诞生的初衷之一,就是把函数式编程的严谨带进企业级 Java 生态——Hadoop 时代的 Spark 选择了 Scala,正是因为它能在分布式计算里把「纯计算」与「副作用」干净地分开。
函数式范式的核心不是「不用变量」这种表面文章,而是三件事:数据默认不可变、函数是一等公民、用类型表达可能性。不可变让并发下的数据竞争从根上消失;函数可传递让逻辑能像搭积木一样组合;类型系统则把「可能失败」「可能没有值」这些状态写进编译期,而不是靠运行时异常。
Scala 特别之处在于它不做「要么面向对象、要么函数式」的单选题。case class 既是对象又是代数数据类型,伴生对象承载工厂方法,模式匹配处理分支——两种范式在一个语法体系里互相成全。本文从四个最有感的抓手切入:不可变集合、高阶函数、模式匹配、Option/Either,最后用 Future 收尾并发。
函数式编程不是「不写变量」,而是让数据流、错误路径和并发边界都变得可见、可组合。
- 不可变val 与不可变集合消灭一类并发 bug。
- 高阶函数map / filter / fold 组合数据流。
- 模式匹配把条件分支写成结构解构。
- Option / Either让「可能没有」「可能失败」进类型。
- Future在容器里编排异步副作用。
不可变数据:val、case class 与集合
Scala 里 val 声明不可变引用,case class 自动生成不可变的字段、相等比较与 toString。集合默认也是不可变的:对 List 做变换返回新集合,原集合纹丝不动。
case class User(name: String, age: Int)
val users = List(
User("张三", 28),
User("李四", 19),
User("王五", 35)
)
val adults = users.filter(_.age >= 18).map(_.name)
// List(张三, 李四, 王五) —— 原 users 没有任何变化
不可变带来的最直接收益在并发:多个线程读同一个不可变集合,不需要任何锁。写代码时也少一类心智负担——函数调用不会「顺手改掉」你传入的数据,调试时可以放心地假设「参数传进去是什么样,出来前就是什么样」。
case class 顺手解决了数据建模:相等比较按内容而非引用,copy 方法可以生成「只有某字段不同」的新实例。这两点让 Scala 数据模型写起来干净,也让后面的模式匹配有了坚实的载体。
函数是一等公民:高阶函数与匿名函数
在 Scala 里,函数可以赋值给变量、作为参数传递、作为返回值。集合的 map / filter / foldLeft 是最高频的高阶函数——它们接收一个函数,对每个元素应用它。
val nums = List(1, 2, 3, 4, 5)
val doubled = nums.map(_ * 2) // List(2,4,6,8,10)
val evens = nums.filter(_ % 2 == 0) // List(2,4)
val total = nums.foldLeft(0)(_ + _) // 15
// 匿名函数完整写法
val plusOne: Int => Int = (x: Int) => x + 1
_ 是 Scala 的位置占位符:_ * 2 等价于 x => x * 2。foldLeft 接受初始值和一个二元函数,把整组元素折叠成一个结果——它是很多聚合逻辑的基石,比手写循环更能表达「累加」这个意图。
高阶函数的意义是把「遍历的控制流」从「具体做什么」里分离:map 负责遍历,函数负责变换。同样的遍历骨架可以换不同的函数,代码复用点从「函数」上移到了「组合」层面。这也是函数式风格最日常的形态。
模式匹配:把分支写成可读的结构
模式匹配是 Scala 的「增强版 switch」:不仅能按值分支,还能解构 case class、绑定变量、加守卫条件。它让「判断 + 取值」两步合并成一次可读的匹配。
def describe(x: Any): String = x match {
case 0 => "零"
case 1 => "一"
case User(n, a) => s"用户 $n 今年 $a 岁"
case s: String => "字符串:" + s
case _ => "其他"
}
注意 case User(n, a):匹配一个 User 实例的同时把 name、age 解构到变量 n 和 a。编译期还会做穷尽性检查——如果 match 的分支没有覆盖所有可能,编译器会警告。这条规则把「漏了一种情况」从运行时错误提前到编译期。
守卫条件(case 后跟 if)让匹配更灵活;case _ 兜底所有剩余情况。用好模式匹配,条件分支代码的可读性会明显上一个台阶:每个分支都自带「结构描述」,而不是一连串 if/else 的嵌套。
Option 与 Either:用类型表达可能失败
Java 处理「没有值」靠 null,处理「失败」靠异常。Scala 更喜欢把这两种状态写进类型:Option[A] 表示「可能有 A,也可能没有」,Either[L, R] 表示「要么是 L(错误),要么是 R(结果)」。
def safeDiv(a: Int, b: Int): Option[Int] =
if (b == 0) None else Some(a / b)
val r1 = safeDiv(6, 2) // Some(3)
val r2 = safeDiv(6, 0) // None
val result = r2.getOrElse(-1) // -1
val mapped = safeDiv(10, 2).map(_ * 2) // Some(10)
Option 的魅力在于它的组合能力:map、flatMap、getOrElse 让「链式处理可能为空的值」不再需要一堆 if (x != null) 守卫。Either 更进一步,能携带错误详情——约定用 Right 装成功、Left 装失败:
def parseAge(s: String): Either[String, Int] =
try Right(s.toInt)
catch { case _: NumberFormatException => Left(s"无法解析:$s") }
把失败变成类型的一部分之后,调用方被迫处理它——编译器会提醒你 Either 需要被解开。这比「函数正常返回、内部偷偷抛异常」的可控性强得多,也是 Scala 函数式代码里错误处理的主流形态。
Future 与并发:把副作用装进容器
函数式代码擅长纯计算,但真实程序总有网络请求、文件读写这类副作用。Scala 的 Future 提供一种折中:把「将要发生的副作用」装进一个可组合的容器,异步执行,但组合逻辑仍然是纯函数式。
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
val f1 = Future { fetchRemote("a") }
val f2 = Future { fetchRemote("b") }
val combined = for {
r1 <- f1
r2 <- f2
} yield r1 + r2
for 推导式在这里是 flatMap/map 的语法糖:r1 <- f1 表示「等 f1 的结果,解包成 r1」,两条 Future 顺序组合。想并发发起两个请求,在 for 外面各自 start 即可;想在结果到达时继续处理,用 .map 挂后续逻辑。
Future 把并发从「手写线程 + 共享状态」里解放出来:你描述的是数据依赖关系,调度交给执行上下文。配合不可变数据,Future 之间的共享变量天然安全。要注意的是 Future 一旦失败,链条上的 map 都会短路——用 recover 接住异常路径,或与 Either 结合,把失败也变成数据。
实战:用函数式风格写一个统计管道
把本文所有技巧组装成一个日志统计管道:过滤成功请求、按路径分组计数、取前 N 名。全程不可变、全程管道式组合。
case class Log(path: String, status: Int)
val logs = List(
Log("/api/a", 200), Log("/api/b", 500),
Log("/api/a", 200), Log("/api/a", 404),
Log("/api/b", 200)
)
val topPaths = logs
.filter(_.status == 200)
.groupBy(_.path)
.map { case (path, items) => (path, items.size) }
.toList
.sortBy(-_._2)
.take(2)
// List((/api/a,2), (/api/b,1))
看这条链:filter 挑成功日志,groupBy 按路径分组,map 配合模式匹配把每组折叠成 (路径, 数量),sortBy 降序,take 取前两名。每一段都是纯函数,输入输出清清楚楚,中间没有一处可变状态。
把这样的管道再套上 Future 或 Either,就能处理「异步 + 可能失败」的真实场景。至此你看到的是 Scala 函数式风格的完整路径:不可变数据打底,高阶函数组合逻辑,模式匹配解构分支,Option/Either 管错误,Future 管并发。五个抓手,一条主线——用类型和组合把复杂度压平。
Scala 函数式编程的终点:让每个变换都可见,让每种失败都可处理,让并发不需要锁。