纯函数

什么是纯函数?

在函数式编程里我们会经常谈到这两个概念。一个是 纯函数。另一个是 附加作用(副作用)。这里我们就结合实际来介绍一下 纯函数附加作用

下面我们给出两个函数 increaseAincreaseB,他们其中一个是 纯函数,另一个不是 纯函数

  1. var state = 0
  2. func increaseA() {
  3. state += 1
  4. }
  5. increaseA()
  6. print(state) // 结果: 1
  1. func increaseB(state: Int) -> Int {
  2. return state + 1
  3. }
  4. let state = increaseB(state: 0)
  5. print(state) // 结果: 1

他们的作用差不多,使 state + 1, 我们可以猜测一下 increaseAincreaseB 哪一个是 纯函数

… 经过 10 秒后

现在公布答案:increaseB纯函数increaseA 不是 纯函数

为什么 increaseB纯函数

因为他特别 纯洁:除了用入参 state 计算返回值以外没做任何其他的事情。

那为什么 increaseA 不是 纯函数

因为他修改了函数本体以外的值 state, 他拥有这个 附加作用,因此他 并不纯洁 就不是 纯函数

我们再来做以下两个测试,然后猜测他们能不能测试成功:

  1. func testIncreaseA() {
  2. increaseA()
  3. state == 1 // 结果:?? 🤔
  4. }
  1. func testIncreaseB() {
  2. let state = increaseB(state: 0)
  3. state == 1 // 结果:true 😎
  4. }

… 经过 20 秒后

嗯… 这里我们可以肯定第二个测试 testIncreaseB 会成功。0 + 1 肯定等于 1。那第一个测试呢?这可不好说了,我们并不知道 increaseA 是在什么环境下被调用的,不知道在这个环境下初始 state 是多少。如果他是 0 那测试就会成功的,如果他不是 0 那测试就会失败的。因此在不知道所处环境时,我们无法判断测试是否会成功。

由于 increaseA 存在修改外部 state附加作用 所以他不是 纯函数。事实上如果函数有以下任意一种作用,他也不是纯函数:

  • 发起网络请求
  • 刷新 UI
  • 读写数据库
  • 获取位置信息
  • 使用蓝牙模块
  • 打印输出

我们将这些作用称为函数的 附加作用(副作用)。

纯函数 的定义就是: 没有 附加作用 的函数,并且在参数相同时,返回值也一定相同。

因此在已知执行逻辑时,纯函数 所产生的结果是可以被预测的。一些现代化的库都利用了这个特性来做状态管理,如:RxFeedbackReduxReactorKit 等等。

纯函数用于状态管理

我们用一个足够简单的例子来演示,如何用 纯函数 做状态管理:

  1. typealias State = Int
  2. enum Event {
  3. case increase
  4. case decrease
  5. }
  6. func reduce(_ state: State, event: Event) -> State {
  7. switch event {
  8. case .increase:
  9. return state + 1
  10. case .decrease:
  11. return state - 1
  12. }
  13. }

这个例子似乎过于简单,以至于我们看不出他有什么特别的。好吧,我承认他的主要目的是向大家演示,用 纯函数 做状态管理的基本单元是什么

首先,我们得有个状态:

  1. typealias State = Int

然后,我们要有各种事件:

  1. enum Event {
  2. case increase
  3. case decrease
  4. }

最后,我们要有一个 纯函数 来管理我们的状态:

  1. func reduce(_ state: State, event: Event) -> State {
  2. switch event {
  3. case .increase:
  4. return state + 1
  5. case .decrease:
  6. return state - 1
  7. }
  8. }

这样,我们就可以做测试了,当 App 处于某个状态时,发生了某个事件,会产生一个结果,这个结果是否符合我们的预期:

  1. func testReduce() {
  2. let state1 = reduce(0, event: .increase)
  3. state1 == 1 // 结果:true 😎
  4. let state2 = reduce(10, event: .decrease)
  5. state2 == 9 // 结果:true 😎
  6. }

以上两个测试都是成功的。当然这里的状态管理过于简单。而真实应用程序的状态都是非常复杂的。并且程序的行为都是很难预测的。要解决这个问题,我们要感谢 纯函数,还记得他的特征吗?

纯函数 在参数相同时,返回值也一定相同。

我们再来看下 reduce 方法:

  1. func reduce(_ state: State, event: Event) -> State { ... }

我们有没有获得一点点灵感…

… 经过 60 秒后

希望你已经获得答案了。

当程序处于某个特定状态时,发生了某个特定事件,会产生某个唯一的结果。这个结果与所处的环境无关,不论是处于应用程序运行环境,还是在测试环境。这个结果只和初始状态以及发生的事件有关。因此,程序的行为是可以被预测的,而且程序运行时的状态更新,可以在测试环境中被模拟出来。

… 经过 60 秒后

现在,我们来看一个相对复杂的例子:

登录状态管理

  1. typealias UserID = String
  2. enum LoginError: Error, Equatable {
  3. case usernamePasswordMismatch
  4. case offline
  5. }
  6. struct State: Equatable {
  7. var username: String
  8. var password: String
  9. var loading: Bool
  10. var data: UserID?
  11. var error: LoginError?
  12. enum Event {
  13. case onUpateUsername(String)
  14. case onUpatePassword(String)
  15. case onTriggerLogin
  16. case onLoginSuccess(UserID)
  17. case onLoginError(LoginError)
  18. }
  19. static func reduce(_ state: State, event: Event) -> State {
  20. var newState = state
  21. switch event {
  22. case .onUpateUsername(let username):
  23. newState.username = username
  24. case .onUpatePassword(let password):
  25. newState.password = password
  26. case .onTriggerLogin:
  27. newState.loading = true
  28. newState.data = nil
  29. newState.error = nil
  30. case .onLoginSuccess(let userId):
  31. newState.loading = false
  32. newState.data = userId
  33. case .onLoginError(let error):
  34. newState.loading = false
  35. newState.error = error
  36. }
  37. return newState
  38. }
  39. }

我们重新走下流程 😄,用 纯函数 做状态管理:

首先,我们得有个状态:

  1. struct State: Equatable {
  2. var username: String // 输入的用户名
  3. var password: String // 输入的密码
  4. var loading: Bool // 登录中
  5. var data: UserID? // 登录成功
  6. var error: LoginError? // 登录失败
  7. ...
  8. }

然后,我们要有各种事件:

  1. enum Event {
  2. case onUpateUsername(String) // 更新用户名
  3. case onUpatePassword(String) // 更新密码
  4. case onTriggerLogin // 触发登录
  5. case onLoginSuccess(UserID) // 登录成功
  6. case onLoginError(LoginError) // 登录失败
  7. }

最后,我们要有一个 纯函数 来管理我们的状态:

  1. static func reduce(_ state: State, event: Event) -> State {
  2. var newState = state
  3. switch event {
  4. case .onUpateUsername(let username):
  5. newState.username = username
  6. case .onUpatePassword(let password):
  7. newState.password = password
  8. case .onTriggerLogin:
  9. newState.loading = true
  10. newState.data = nil
  11. newState.error = nil
  12. case .onLoginSuccess(let userId):
  13. newState.loading = false
  14. newState.data = userId
  15. case .onLoginError(let error):
  16. newState.loading = false
  17. newState.error = error
  18. }
  19. return newState
  20. }

现在我们可以在测试环境模拟各种事件,并且判断结果是否符合预期:

  • 更新用户名事件
  1. func testOnUpateUsername() {
  2. let state = State(
  3. username: "",
  4. password: "",
  5. loading: false,
  6. data: nil,
  7. error: nil
  8. )
  9. let newState = State.reduce(state, event: .onUpateUsername("beeth0ven"))
  10. let expect = State(
  11. username: "beeth0ven",
  12. password: "",
  13. loading: false,
  14. data: nil,
  15. error: nil
  16. )
  17. newState == expect // 结果:true 😎
  18. }
  • 更新密码事件
  1. func testOnUpatePassword() {
  2. let state = State(
  3. username: "beeth0ven",
  4. password: "",
  5. loading: false,
  6. data: nil,
  7. error: nil
  8. )
  9. let newState = State.reduce(state, event: .onUpatePassword("123456"))
  10. let expect = State(
  11. username: "beeth0ven",
  12. password: "123456",
  13. loading: false,
  14. data: nil,
  15. error: nil
  16. )
  17. newState == expect // 结果:true 😎
  18. }
  • 触发登录事件
  1. func testOnTriggerLogin() {
  2. let state = State(
  3. username: "beeth0ven",
  4. password: "123456",
  5. loading: false,
  6. data: nil,
  7. error: nil
  8. )
  9. let newState = State.reduce(state, event: .onTriggerLogin)
  10. let expect = State(
  11. username: "beeth0ven",
  12. password: "123456",
  13. loading: true,
  14. data: nil,
  15. error: nil
  16. )
  17. newState == expect // 结果:true 😎
  18. }
  • 登录成功事件
  1. func testOnLoginSuccess() {
  2. let state = State(
  3. username: "beeth0ven",
  4. password: "123456",
  5. loading: true,
  6. data: nil,
  7. error: nil
  8. )
  9. let newState = State.reduce(state, event: .onLoginSuccess("userID007"))
  10. let expect = State(
  11. username: "beeth0ven",
  12. password: "123456",
  13. loading: false,
  14. data: "userID007",
  15. error: nil
  16. )
  17. newState == expect // 结果:true 😎
  18. }
  • 登录失败事件
  1. func testOnLoginError() {
  2. let state = State(
  3. username: "beeth0ven",
  4. password: "123456",
  5. loading: true,
  6. data: nil,
  7. error: nil
  8. )
  9. let newState = State.reduce(state, event: .onLoginError(.usernamePasswordMismatch))
  10. let expect = State(
  11. username: "beeth0ven",
  12. password: "123456",
  13. loading: false,
  14. data: nil,
  15. error: .usernamePasswordMismatch
  16. )
  17. newState == expect // 结果:true 😎
  18. }

这样我们可以轻易掌控程序的运行状态,以及各种状态更新。

现在,我们知道如何用 纯函数 做状态管理了。不过当前的代码形态,离投入生产环境,还存在好几个过度形态。这些过度形态有的是围绕如何引入 附加作用,而做了一些应用架构。在这个问题上,不同地架构也提出了不同的解决方案,如:RxFeedbackfeedbackLoop 引入 附加作用Reduxmiddleware 引入 附加作用 等等。这里就不一一介绍了,这些库的官方网站都会有相关说明。

最后,我们还是将代码演化到下一个形态,这里我选择使用 Redux 流派。因为个人的觉得他的知识依赖要少一些,可以让更多读者从中获益。

下一步 — 引入 Store

  1. class Store {
  2. // 观察者,用于响应状态更新,第一个 State? 为旧状态,第二个 State 为当前状态
  3. typealias Observer = (State?, State) -> Void
  4. private(set) var state: State // 当前状态
  5. private var _observers: [UUID: Observer] // 所有的观察者
  6. // 初始化
  7. init(initailState: State) {
  8. self.state = initailState
  9. self._observers = [:]
  10. }
  11. // 发出事件
  12. func dispatch(event: State.Event) {
  13. let oldState = self.state
  14. self.state = State.reduce(self.state, event: event)
  15. _publish(oldState: oldState, newState: self.state)
  16. }
  17. // 订阅状态更新
  18. func subscribe(observer: @escaping Observer) -> UUID {
  19. let subscriptionID = UUID() // UUID 是唯一标识符,该 id 可用于取消订阅
  20. _observers[subscriptionID] = observer
  21. observer(nil, self.state) // 订阅时,将当前状态回放给该观察者
  22. return subscriptionID
  23. }
  24. // 取消订阅
  25. func unsubscribe(_ subscriptionID: UUID) {
  26. _observers.removeValue(forKey: subscriptionID)
  27. }
  28. // 私有方法,通知所有的观察者,状态已经更新了
  29. private func _publish(oldState: State?, newState: State) {
  30. _observers.values.forEach { observer in
  31. observer(oldState, newState)
  32. }
  33. }
  34. }

如何使用 Store:

  1. func useStore() {
  2. let initailState = State(
  3. username: "",
  4. password: "",
  5. loading: false,
  6. data: nil,
  7. error: nil
  8. )
  9. let store = Store(initailState: initailState)
  10. // 以下变量 newStates 和 oldStates 用于录制状态历史
  11. var newStates: [State] = []
  12. var oldStates: [State?] = []
  13. let subscriptionID = store.subscribe { (oldState, newState) in
  14. newStates.append(newState)
  15. oldStates.append(oldState)
  16. }
  17. // 模拟真实事件
  18. store.dispatch(event: .onUpateUsername("beeth0ven"))
  19. store.dispatch(event: .onUpatePassword("123456"))
  20. // 取消订阅
  21. store.unsubscribe(subscriptionID)
  22. // 描叙预期
  23. let expectNewStates = [
  24. State(
  25. username: "",
  26. password: "",
  27. loading: false,
  28. data: nil,
  29. error: nil
  30. ),
  31. State(
  32. username: "beeth0ven",
  33. password: "",
  34. loading: false,
  35. data: nil,
  36. error: nil
  37. ),
  38. State(
  39. username: "beeth0ven",
  40. password: "123456",
  41. loading: false,
  42. data: nil,
  43. error: nil
  44. )
  45. ]
  46. let expectOldStates = [
  47. nil,
  48. State(
  49. username: "",
  50. password: "",
  51. loading: false,
  52. data: nil,
  53. error: nil
  54. ),
  55. State(
  56. username: "beeth0ven",
  57. password: "",
  58. loading: false,
  59. data: nil,
  60. error: nil
  61. )
  62. ]
  63. // 比对结果
  64. newStates == expectNewStates // 结果:true 😎
  65. oldStates == expectOldStates // 结果:true 😎
  66. }

以上是在单元测试环境下,

首先下创建 Store:

  1. let initailState = State(
  2. username: "",
  3. password: "",
  4. loading: false,
  5. data: nil,
  6. error: nil
  7. )
  8. let store = Store(initailState: initailState)

然后,订阅程序状态,并且将这些状态录制下来:

  1. var newStates: [State] = []
  2. var oldStates: [State?] = []
  3. let subscriptionID = store.subscribe { (oldState, newState) in
  4. newStates.append(newState)
  5. oldStates.append(oldState)
  6. }

然后,模拟输入用户名事件和输入密码事件:

  1. store.dispatch(event: .onUpateUsername("beeth0ven"))
  2. store.dispatch(event: .onUpatePassword("123456"))

然后,取消订阅:

  1. store.unsubscribe(subscriptionID)

最后,比对录制的状态是否符合预期:

  1. let expectNewStates = [
  2. State(
  3. username: "",
  4. password: "",
  5. loading: false,
  6. data: nil,
  7. error: nil
  8. ),
  9. State(
  10. username: "beeth0ven",
  11. password: "",
  12. loading: false,
  13. data: nil,
  14. error: nil
  15. ),
  16. State(
  17. username: "beeth0ven",
  18. password: "123456",
  19. loading: false,
  20. data: nil,
  21. error: nil
  22. )
  23. ]
  24. let expectOldStates = [
  25. nil,
  26. State(
  27. username: "",
  28. password: "",
  29. loading: false,
  30. data: nil,
  31. error: nil
  32. ),
  33. State(
  34. username: "beeth0ven",
  35. password: "",
  36. loading: false,
  37. data: nil,
  38. error: nil
  39. )
  40. ]
  41. newStates == expectNewStates // 结果:true 😎
  42. oldStates == expectOldStates // 结果:true 😎

这就是如何在测试环境里面使用 Store,那么在 App 里面如何使用 Store 呢。 一个 相对简单(并未优化) 的方法,就是将 Store 注入到对应的组件里面,这里以 ViewController 为例:

  • ViewController 可以使用 store.subscribe 方法订阅程序的状态。当状态更新时,比对新旧状态,然后刷新过时了的 UI。
  • 当用户触发某个事件时,调用 store.dispatch 方法将事件发出去,如:当用户点击登录按钮时,就调用 store.dispatch(event: .onTriggerLogin)
  • ViewControllerdeinit 方法里面注销订阅 store.unsubscribe(subsriptionID)

总结

本节主要介绍了 纯函数附加作用,期间还演示如何用 纯函数 做状态管理的。最后还演化出了一个极简版的 Redux。希望大家可以从中获益!

参考