Scala 函数式编程实战:不可变、纯函数、高阶函数与 Option

系统讲解 Scala 函数式编程核心范式:不可变与纯函数、高阶函数(map/flatMap/foldLeft)、函数组合、Option/Either 错误处理、模式匹配、隐式参数与类型类初探,并给出从命令式思维迁移到函数式思维的方法论。

引言

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. 不可变与纯函数:函数式的地基

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

维度nullOption
类型表达无(类型不区分可空)类型明确 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

维度switchmatch
匹配类型值值/类型/结构/守卫
可穷尽检查无有
返回值无(语句)有(表达式)
解构无有

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

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「scala」更多文章

  1. 纯函数式效果系统实战:Cats Effect IO 与 ZIO
  2. Scala.js 与 Scala Native:跨平台编译、互操作与工程实践
  3. Scala 领域建模实战:ADT、类型驱动设计与模块化架构