引言
Scala 是一门多范式语言,但函数式编程(FP)是它区别于 Java 的「灵魂」。很多 Java 开发者转向 Scala,最先遇到的困惑不是语法,而是思维模式的转变:不再问「怎么改状态」,而是问「怎么用表达式描述计算」。
本文从四个核心概念讲透函数式编程:不可变与纯函数(为什么副作用是万恶之源)、高阶函数(函数作为一等公民,map/flatMap/foldLeft 的思维)、Option/Either(用类型表达可能失败)、模式匹配与类型类(把分支与行为组织起来)。全程可运行代码 + Java 对照,帮你完成从命令式到函数式的思维迁移。
前置:Scala 基础语法。若需 Scala 开发环境与 sbt 配置,可参考 sbt 官方文档。并发与 Actor 范式见 https://plumephp.com/actor-model-detailed-explanation/ 及本专题 Akka 文章。
目录
- 1. 不可变与纯函数:函数式的地基
- 2. 高阶函数:函数是一等公民
- 3. 集合操作思维:map、filter、flatMap 与 fold
- 4. Option 与 Either:用类型表达失败
- 5. 模式匹配:结构化分支
- 6. 函数组合:管道与流水线
- 7. 隐式参数与类型类
- 8. 从命令式迁移:思维转变方法论
- 9. 总结:函数式编程的四个支柱
- 延伸阅读
1. 不可变与纯函数:函数式的地基
1.1 纯函数:同样的输入,同样的输出
纯函数满足两个条件:无副作用(不修改外部状态)、确定性(同样的输入永远同样的输出)。
// ❌ 不纯:读/写了外部状态
var cache: Map[String, Int] = Map.empty
def slowAdd(a: Int, b: Int): Int = {
val key = s"$a+$b"
cache.getOrElse(key, { val r = a + b; cache += key -> r; r })
}
// ✅ 纯:不依赖也不修改外部状态
def add(a: Int, b: Int): Int = a + b
1.2 为什么纯函数好
| 性质 | 收益 |
|---|---|
| 确定性 | 可测试、可缓存、可并行 |
| 无副作用 | 无竞态、无隐藏依赖 |
| 可组合 | 像积木一样拼装 |
1.3 不可变:val 优先
val nums = List(1, 2, 3) // 不可变,add 返回新 List
val doubled = nums.map(_ * 2) // List(2,4,6)
不可变数据是并发的安全基石:没有共享可变状态,就没有锁。
2. 高阶函数:函数是一等公民
2.1 函数可以像值一样传递
val addOne: Int => Int = (x: Int) => x + 1
val addTwo = (x: Int) => x + 2
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(addOne, 5) // 7
2.2 常用高阶函数
List(1,2,3).map(_ * 2) // 逐个变换
List(1,2,3,4).filter(_ % 2 == 0) // 按条件保留
List(1,2,3).reduceLeft(_ + _) // 两两归并
2.3 方法转函数(Eta 扩展)
def isEven(x: Int): Boolean = x % 2 == 0
val f: Int => Boolean = isEven _ // 方法提升为函数
List(1,2,3,4).filter(f) // List(2,4)
3. 集合操作思维:map、filter、flatMap 与 fold
3.1 三件套的直觉
| 操作 | 做什么 | Java 对照 |
|---|---|---|
map | 每个元素变换 | stream.map(...) |
filter | 按条件筛选 | stream.filter(...) |
flatMap | 变换后再拍平 | stream.flatMap(...) |
val words = List("hello", "world")
words.map(_.toUpperCase) // List("HELLO","WORLD")
words.flatMap(_.toList) // 拆成字符并拍平:List(h,e,l,l,o,...)
3.2 fold:用初始值累积
List(1,2,3,4).foldLeft(0)(_ + _) // 10,从左累积
List(1,2,3,4).foldLeft(1)(_ * _) // 24
fold 是「把集合压缩成一个值」的通用工具,很多聚合操作都是它的特例。
3.3 for 推导式:flatMap 的语法糖
val a = List(1, 2)
val b = List(3, 4)
// 等价写法 1:flatMap 嵌套
val r1 = a.flatMap(x => b.map(y => (x, y)))
// 等价写法 2:for 推导式(更可读)
val r2 = for {
x <- a
y <- b
} yield (x, y)
// 两者都得到 List((1,3),(1,4),(2,3),(2,4))
for 推导式是理解 Scala 函数式代码的钥匙:它是 map/flatMap/filter 的语法糖,不是循环。
4. Option 与 Either:用类型表达失败
4.1 Option:可能没有值
def findUser(id: Int): Option[User] =
if (id > 0) Some(User(id)) else None
findUser(1) match {
case Some(u) => s"找到: $u"
case None => "未找到"
}
4.2 用 map/flatMap 链式处理 Option
// 避免层层 if-null
val result = findUser(1)
.flatMap(u => u.email) // 可能没有 email
.map(e => e.toLowerCase)
.getOrElse("no-email")
4.3 Either:不仅知道失败,还知道为什么
// 约定:Left 是错误,Right 是成功
def parseAge(s: String): Either[String, Int] =
s.toIntOption.map(Right(_)).getOrElse(Left(s"无法解析: $s"))
parseAge("30") // Right(30)
parseAge("abc") // Left("无法解析: abc")
4.4 Option 对比 Java 的 null
| 维度 | null | Option |
|---|---|---|
| 类型表达 | 无(类型不区分可空) | 类型明确 Option[T] |
| 空指针异常 | 有 | 无 |
| 链式处理 | 层层判空 | map/flatMap |
| 可读性 | 隐式 | 显式 |
5. 模式匹配:结构化分支
5.1 基本用法
def describe(x: Any): String = x match {
case i: Int if i > 0 => "正整数"
case i: Int => "非正整数"
case s: String => s"字符串: $s"
case List(a, b, _*) => s"列表,前两个: $a, $b"
case _ => "未知"
}
5.2 模式匹配 vs switch
| 维度 | switch | match |
|---|---|---|
| 匹配类型 | 值 | 值/类型/结构/守卫 |
| 可穷尽检查 | 无 | 有 |
| 返回值 | 无(语句) | 有(表达式) |
| 解构 | 无 | 有 |
5.3 用 case class 做结构解构
case class Order(id: Long, amount: Double)
def fee(o: Order): Double = o match {
case Order(_, a) if a > 1000 => a * 0.01 // 解构出 amount
case _ => 0.0
}
6. 函数组合:管道与流水线
6.1 组合子
val double = (x: Int) => x * 2
val inc = (x: Int) => x + 1
val doubleThenInc = inc.compose(double) // 先 double 再 inc
val incThenDouble = inc.andThen(double) // 先 inc 再 double
doubleThenInc(3) // 7
incThenDouble(3) // 8
6.2 流水线风格
// 把一次数据处理写成一段「管道」
val pipeline = List(1,2,3,4,5)
.filter(_ > 2) // 过滤
.map(_ * 10) // 放大
.foldLeft(0)(_ + _) // 累加
// = (30 + 40 + 50) = 120
6.3 为什么这样写可读
每步是声明「要什么」而非「怎么改」,代码自解释,也天然可并行(多数操作无共享状态)。
7. 隐式参数与类型类
7.1 隐式参数:自动补全依赖
// 泛型序列化:调用方无需显式传序列化器
trait Serializer[T] { def serialize(t: T): String }
given intSerializer: Serializer[Int] =
(t: Int) => t.toString
def toJson[T](t: T)(using s: Serializer[T]): String =
s.serialize(t)
toJson(42) // 自动找 implicit Serializer[Int]
7.2 类型类模式
类型类(Type Class)是「为类型添加行为」的 FP 惯用法:定义 trait + 若干实例 + 约束使用。Scala 3 用 given/using 替代 Scala 2 的 implicit。
7.3 隐式的作用
| 用途 | 说明 |
|---|---|
| 类型类 | 多态行为 |
| 上下文约束 | [T: Serializer] 语法糖 |
| 隐式转换 | 新版本谨慎使用(易出 bug) |
8. 从命令式迁移:思维转变方法论
8.1 三步改写练习
// 第1步:命令式(Java 风格)
def sumEven(list: List[Int]): Int = {
var acc = 0
for (x <- list) {
if (x % 2 == 0) acc += x
}
acc
}
// 第2步:逐步函数式
def sumEven2(list: List[Int]): Int =
list.filter(_ % 2 == 0).foldLeft(0)(_ + _)
// 第3步:组合成纯表达式
val sumEven3 = (list: List[Int]) =>
list.filter(_ % 2 == 0).sum
8.2 迁移常见误区
| 误区 | 纠正 |
|---|---|
| 「可变才快」 | 不可变 + JVM 现代 GC 足够 |
| 「没有 for 循环就不会写了」 | 用 map/fold 表达循环 |
| 「Option 麻烦」 | 显式失败比 null 更安全 |
| 「FP 只适合玩具」 | 大型系统已大量应用 |
8.3 从 Scala 2 到 Scala 3
Scala 3 让 FP 更自然:given/using、enum、缩进语法、* 通配符。详见本专题 https://plumephp.com/scala3-modern-features/ 文章。
9. 总结:函数式编程的四个支柱
9.1 一句话心法
| 支柱 | 要点 |
|---|---|
| 纯函数 | 无副作用、确定性 |
| 不可变 | val 优先,数据安全并发 |
| 高阶函数 | 函数是一等公民,map/fold 组织计算 |
| 类型表达 | Option/Either 显式失败,模式匹配结构化分支 |
函数式编程不是「不用变量」,而是「用表达式描述计算、用类型约束正确性」。
延伸阅读
- https://plumephp.com/actor-model-detailed-explanation/ — Actor 并发模型(Scala 生态并发另一支柱)
- https://plumephp.com/scala-collections/ — 不可变集合库深入
- https://plumephp.com/scala-type-system/ — 类型系统与类型类深化
- Scala 官方文档 与 Functional Programming in Scala
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。