3-13.【函数式编程】讨论函数式编程中如何管理 I/O、网络请求或日志打印等副作用。

3 阅读3分钟

1️⃣ 副作用的问题

副作用包括:

  • I/O 操作:读取文件、写入文件、打印、数据库访问
  • 网络请求:调用 API、上传/下载数据
  • 系统状态:修改全局变量、发送通知

问题:

  • 同样输入可能产生不同输出
  • 函数无法纯粹组合
  • 并发时容易出现竞态条件

目标:将副作用隔离或延迟执行,保持核心逻辑纯净。


2️⃣ 函数式编程处理副作用的思路

(1) 使用“描述性”数据结构来表示副作用

  • FP 中常用 IO MonadEffect 类型来“描述”副作用,而不立即执行
  • 函数返回 描述副作用的值,由顶层调用者统一执行

伪代码示例(概念):

// 描述一个网络请求的操作,而不立即发起
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) 分离副作用和纯逻辑

将函数分成两层:

  1. 纯函数层 → 业务逻辑、数据转换
  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 特性管理副作用

  1. 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 内
  • 核心转换操作保持纯净
  1. 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 管理副作用原则

  1. 延迟副作用 → 不在核心函数中立即执行 I/O
  2. 隔离副作用 → 副作用集中在最外层调用者或专门模块
  3. 描述副作用 → 使用数据结构或类型封装操作
  4. 保持核心逻辑纯净 → 便于测试、组合、并发安全

4️⃣ Swift 实践建议

场景FP 管理副作用方法
网络请求使用 async/awaitCombine 封装请求,核心逻辑纯函数处理数据
日志打印使用 Writer-like 模式或延迟打印,函数返回日志字符串
文件 I/O返回文件操作描述或数据,执行操作放在最外层
状态修改使用值类型 (struct) + 返回新状态,而不是修改全局对象

💡 经验法则

在 Swift FP 中,核心逻辑尽量纯净,所有 I/O、网络请求、打印副作用延迟到最外层执行,这样代码更安全、可测试、易组合。