从 JVM 语言到函数式范式

Scala 是一门站在 JVM 上的多范式语言:既能像 Java 一样写面向对象,也能流畅地写函数式代码。它诞生的初衷之一,就是把函数式编程的严谨带进企业级 Java 生态——Hadoop 时代的 Spark 选择了 Scala,正是因为它能在分布式计算里把「纯计算」与「副作用」干净地分开。

函数式范式的核心不是「不用变量」这种表面文章,而是三件事:数据默认不可变、函数是一等公民、用类型表达可能性。不可变让并发下的数据竞争从根上消失;函数可传递让逻辑能像搭积木一样组合;类型系统则把「可能失败」「可能没有值」这些状态写进编译期,而不是靠运行时异常。

Scala 特别之处在于它不做「要么面向对象、要么函数式」的单选题。case class 既是对象又是代数数据类型,伴生对象承载工厂方法,模式匹配处理分支——两种范式在一个语法体系里互相成全。本文从四个最有感的抓手切入:不可变集合、高阶函数、模式匹配、Option/Either,最后用 Future 收尾并发。

函数式编程不是「不写变量」,而是让数据流、错误路径和并发边界都变得可见、可组合。
本文核心知识点
  • 不可变val 与不可变集合消灭一类并发 bug。
  • 高阶函数map / filter / fold 组合数据流。
  • 模式匹配把条件分支写成结构解构。
  • Option / Either让「可能没有」「可能失败」进类型。
  • Future在容器里编排异步副作用。
Scala 函数式编程实战配图:深蓝至蓝紫渐变等距插画,左侧发光轨道上排列多个带不同几何图形的半透明方块,经管道与菱形分岔后汇聚到右侧阶梯状上升的粉色发光菱形模块平台,上方一个半透明箭头指向斜上方
几何方块经函数管道变换后落到结果平台——不可变函数把输入与输出分离的视觉写照。

不可变数据: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 函数式代码里错误处理的主流形态。