1️⃣ 副作用的问题
副作用包括:
- I/O 操作:读取文件、写入文件、打印、数据库访问
- 网络请求:调用 API、上传/下载数据
- 系统状态:修改全局变量、发送通知
问题:
- 同样输入可能产生不同输出
- 函数无法纯粹组合
- 并发时容易出现竞态条件
目标:将副作用隔离或延迟执行,保持核心逻辑纯净。
2️⃣ 函数式编程处理副作用的思路
(1) 使用“描述性”数据结构来表示副作用
- FP 中常用 IO Monad 或 Effect 类型来“描述”副作用,而不立即执行
- 函数返回 描述副作用的值,由顶层调用者统一执行
伪代码示例(概念):
// 描述一个网络请求的操作,而不立即发起
struct NetworkRequest {
let url: URL
func execute() -> String {
// 发起请求,返回结果
}
}
func fetchUser(id: Int) -> NetworkRequest {
return NetworkRequest(url: URL(string: "https://api.example.com/user/(id)")!)
}
// 调用者负责执行
let request = fetchUser(id: 123)
let result = request.execute() // 副作用在这里才发生
优势:
- 核心函数
fetchUser是纯函数 - 副作用被封装、延迟执行
- 易于测试(可以用模拟 request)
(2) 分离副作用和纯逻辑
将函数分成两层:
- 纯函数层 → 业务逻辑、数据转换
- 副作用层 → 读取、写入、打印
示例:处理数据并写日志
func processData(_ input: String) -> String {
return input.uppercased() // 纯函数
}
// 副作用层
func log(_ message: String) {
print(message)
}
// 使用
let data = "hello"
let result = processData(data) // 核心逻辑纯净
log("Processed: (result)") // 副作用集中在这里
- 核心逻辑可以单元测试
- 副作用集中、易管理
(3) 使用函数式 Swift 特性管理副作用
- Combine / AsyncSequence / Swift Concurrency
- 网络请求、I/O、异步操作可以用 Publisher / async/await 封装
- 核心函数仍然是纯逻辑
- 副作用在 Subscriber / await 时执行
import Foundation
import Combine
func fetchData(url: URL) -> AnyPublisher<Data, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data } // 纯转换
.eraseToAnyPublisher()
}
// 使用
let url = URL(string: "https://api.example.com")!
let cancellable = fetchData(url: url)
.sink(receiveCompletion: { _ in }, receiveValue: { data in
print("Received (data.count) bytes") // 副作用在这里
})
- 副作用被延迟到 sink 内
- 核心转换操作保持纯净
- Result / Either 类型
- 可以将错误处理也封装在类型中,不在纯函数中直接抛出副作用
func parseInt(_ str: String) -> Result<Int, Error> {
if let n = Int(str) { return .success(n) }
else { return .failure(ParseError.invalid) }
}
- 副作用(打印、写文件)不混入核心解析逻辑
(4) 日志打印的纯函数式管理
- FP 中常用 Writer Monad 或“日志累积”方式
- 函数返回
(值, 日志),日志作为值传递而不是直接打印
func addWithLog(_ a: Int, _ b: Int) -> (Int, String) {
let result = a + b
return (result, "Added (a) + (b) = (result)")
}
// 使用
let (sum, log) = addWithLog(2, 3)
print(log) // 副作用集中在这里
- 核心逻辑纯函数
- 日志可以延迟打印或测试
3️⃣ 总结 FP 管理副作用原则
- 延迟副作用 → 不在核心函数中立即执行 I/O
- 隔离副作用 → 副作用集中在最外层调用者或专门模块
- 描述副作用 → 使用数据结构或类型封装操作
- 保持核心逻辑纯净 → 便于测试、组合、并发安全
4️⃣ Swift 实践建议
| 场景 | FP 管理副作用方法 |
|---|---|
| 网络请求 | 使用 async/await 或 Combine 封装请求,核心逻辑纯函数处理数据 |
| 日志打印 | 使用 Writer-like 模式或延迟打印,函数返回日志字符串 |
| 文件 I/O | 返回文件操作描述或数据,执行操作放在最外层 |
| 状态修改 | 使用值类型 (struct) + 返回新状态,而不是修改全局对象 |
💡 经验法则:
在 Swift FP 中,核心逻辑尽量纯净,所有 I/O、网络请求、打印副作用延迟到最外层执行,这样代码更安全、可测试、易组合。