Scala 函数式编程 (第二版)

Functional Programming in Scala, Second Edition

互动摘要 | 核心概念翻转卡片 | 事实与观点

Part 1: 函数式编程基础 (Introduction)
第1章:什么是函数式编程?

本章确立了全书的基础:FP 是关于使用纯函数构建程序。

核心事实: 函数式编程是一种限制,它限制了我们编写程序的方式(不使用副作用),但并没有限制我们所能表达的程序类型。

通过消除副作用,我们获得了模块化,使程序更容易测试、复用、并行化和推理。

副作用 (Side Effects)

点击翻转查看定义
除了返回结果外,函数还做了其他事情(如修改变量、打印日志、抛出异常)。FP 旨在消除这些。

纯函数 (Pure Function)

点击翻转查看定义
一个没有副作用的函数。对于相同的输入,总是返回相同的输出。

引用透明性 (Referential Transparency)

点击翻转查看定义
如果一个表达式可以被它的计算结果替换而不改变程序的含义,则该表达式是引用透明的。
第2章:Scala 函数式编程起步

介绍了 Scala 语言基础以及如何编写循环和高阶函数。

观点: 任何循环都可以重写为递归函数。

尾递归 (Tail Recursion)

@tailrec
递归调用发生在函数的最后一步。编译器可以将其优化为 while 循环,避免栈溢出。

高阶函数 (Higher-Order Functions)

函数作为一等公民
接受函数作为参数,或者返回一个函数的函数。
第3章:函数式数据结构

讨论了不可变数据结构(如 List, Tree)的定义和操作。

事实: 函数式数据结构通过数据共享 (Data Sharing) 来实现效率,避免了防御性复制。

代数数据类型 (ADT)

enum / sealed trait
由一个或多个数据构造器定义的数据类型。通常是 Sum Type (和类型) 和 Product Type (积类型) 的组合。

模式匹配 (Pattern Matching)

match case
类似于 switch,但能深入解构数据结构的复杂逻辑。它是处理 ADT 的主要方式。
第4章:不使用异常处理错误

异常破坏了引用透明性。本章引入了将错误作为值返回的概念。

Option[A]

Some(a) | None
表示一个可能存在也可能不存在的值。

Either[E, A]

Left(e) | Right(a)
表示两种可能类型之一。通常 Left 用于表示错误(Error),Right 用于表示正确(Correct)。
第5章:严格性与惰性 (Strictness and Laziness)

介绍了非严格求值(惰性求值)以及如何利用它提高效率和模块化。

核心观点: 惰性允许我们将表达式的描述表达式的求值分离开来。

Thunk

() => A
一个未被求值的表达式,在 Scala 中通常通过传名参数 (=> A) 实现。

LazyList (Stream)

智能构造器
元素的计算被推迟到真正需要时才进行。允许定义无限列表。
第6章:纯函数式状态

如何在该没有副作用的情况下处理状态变更(如随机数生成)。

状态转移

S => (A, S)
不在原地修改变量,而是接受一个状态,返回一个结果值和一个新的状态。

State Monad

State[S, A]
封装了状态传递逻辑的类型,使得我们可以用看似命令式的方式(如 for-comprehension)编写纯函数式代码。
Part 2: 函数式设计与组合子库
第7章:纯函数式并行

通过设计一个并行库(Par)来展示函数式库的设计过程。

设计原则: 先设计代数(API 和定律),再考虑实现。关注描述与执行的分离。

代数设计 (Algebraic Design)

API First
这种方法先规定接口和定律,然后由定律指导数据类型的表示选择。

Forking Law

fork(x) == x
这个定律揭示了如果实现不当(如使用固定大小线程池且立即执行),可能导致死锁。
第8章:基于属性的测试

设计一个库来自动验证程序是否满足特定属性(Property)。

生成器 (Generator)

Gen[A]
知道如何生成测试数据的组件。可以组合、转换。

测试用例最小化 (Shrinking)

Minimization
当测试失败时,框架尝试找到导致失败的最小、最简单的输入,便于调试。
第9章:解析器组合子 (Parser Combinators)

构建一个用于文本解析的库。通过组合小的解析器来构建复杂的解析器。

事实: 解析器本质上是一个接受输入并返回结果(或错误)的函数,通常带有状态(如当前位置)。

上下文敏感性 (Context Sensitivity)

flatMap
后续的解析行为依赖于前面的解析结果(例如:先解析一个数字N,再解析N个字符)。需要 flatMap 支持。
Part 3: 函数式设计的通用结构
第10章:Monoid (幺半群)

介绍纯代数结构。Monoid 是最简单的结构之一。

Monoid 定义

结合律 + 幺元
类型 A,结合性二元操作 op(x,y),以及单位元 zero。例子:(Int, +, 0), (String, +, "").

可折叠 (Foldable)

折叠数据结构
任何 Foldable 结构中的元素如果是一个 Monoid,就可以轻易地通过 combine 操作被归约为一个值。
第11章:Monad (单子)

统一了之前的 Parser, Gen, Par, Option, State 等模式。Monad 是比 Applicative 更强大的抽象。

核心能力: Monad 允许上下文敏感的计算,即后续计算可以依赖于前一步计算的

Monad 最小原语

unit + flatMap
或者 unit + compose, 或者 unit + map + join。它们都必须满足结合律和同一律。

Kleisli 组合

A => F[B]
Monad 的结合律通过 Kleisli 箭头的组合 (compose) 来看最为清晰。
第12章:应用函子与可遍历函子 (Applicative & Traversable)

Applicative 比 Monad 弱(更通用),Traversable 允许在保留结构的同时处理副作用。

观点: 如果你不需要 Monad 的全部能力(即不需要上下文敏感性),优先使用 Applicative,因为它更容易组合和并行化。

Applicative

map2 + unit
这里的计算结构是固定的(静态的)。不像 Monad,所有效果可以并行发生,因为它们不相互依赖。

Traversable

sequence / traverse
将 F[G[A]] 翻转为 G[F[A]]。例如将 List[Option[A]] 转换为 Option[List[A]]。
Part 4: 作用与 I/O
第13章:外部作用与 I/O

如何纯粹地处理外部世界。区分了“副作用 (Side Effect)”与“作用 (Effect)”。

IO Monad

描述 vs 执行
IO[A] 只是一个关于计算的描述。只有在 interpreter(解释器)运行时,副作用才会发生。

Free Monad

代数与解释器分离
允许我们将程序的业务逻辑(代数)与具体的执行方式(解释器)完全解耦。

Trampolining

栈安全
用堆上的循环代替栈上的递归,解决 Monad 组合时的 StackOverflowError 问题。
第14章:局部作用与可变状态

如果副作用对外不可见,那么它就是纯的。本章引入了 ST Monad 来处理局部可变状态。

核心概念: 纯度是上下文相关的。 如果我们能在类型系统中证明可变状态不会逃逸出局部作用域,那么整个函数就是纯的。

ST Monad

局部状态线程
利用 Scala 的类型系统(Rank-2 Polymorphism)保证可变引用(STRef/STArray)绝不会泄露到创建它的作用域之外。
第15章:流处理与增量 I/O

解决 IO Monad 无法高效、组合式地处理大数据流的问题。

问题: 简单的 IO Monad 往往导致即使读取大文件也必须一次性读入内存,或者必须编写巨大的 monolithic 循环,无法组合。

Stream / Pull

增量处理
定义了能够按需生成元素、处理错误并确保资源安全释放(Resource Safety)的流式处理模型。

Pipe

转换流
独立的流转换逻辑,可以将输入流转换为输出流,并可以像函数一样组合。

🌟 关键金句

"FP 是一种对我们编写程序方式的限制,而不是对我们所能表达的程序内容的限制。"
"每一个带有副作用的函数内部,都潜藏着一个纯函数等待被释放。"
"Monoid 是关于折叠(Folding)的,Monad 是关于测序(Sequencing)的。"
"引用透明性强制了一个不变量:函数所做的一切都由其返回值表示。"
"我们并不是在编写可以被测试的程序,而是在编写可以被证明的程序。"

原文

源链接