Effect as a Combine publisher

重构非常简单,过程中只有一些曲折。也许最简单的开始方法是简单地注释掉Effect类型,并用指向publisher的类型别名替换它。然而,由于Publisher是一个协议,我们不能直接使用它,我们必须实际选择一个具体的Publisher。我们可以使用苹果提供的AnyPublisher实现一致性,但拥有自己的命名类型会很方便,这样我们就可以在不污染AnyPublisher的情况下为它添加特定的帮助和扩展。

所以,让我们尝试从零开始创造一个合格的publisher:

public struct Effect: Publisher {
}

🛑 Type ‘Effect’ does not conform to protocol ‘Publisher’

我们需要实现一些一致性,所以让我们看看需要什么:

public struct Effect: Publisher {
  public typealias Output = <#type#>
  public typealias Failure = <#type#>

}

我们需要一个Output和Failure类型。记住,effect的唯一目的是最终产生一个反馈到store的动作。即使effect在某种程度上出错,比如设备离线时的网络请求,它仍然需要产生一个动作。因此,效果可以在动作中放入Result值来表示失败,但effect publisher本身不能失败。这意味着我们应该将Never用于publisherFailure类型:

public struct Effect: Publisher {
  public typealias Output = <#type#>
  public typealias Failure = Never
}

另一方面,这种类型的用户需要确定输出,所以它应该是通用的:

public struct Effect<Output>: Publisher {
  public typealias Failure = Never
}

完成后还有一个要求:

public func receive<S>(
  subscriber: S
) where S: Subscriber, Failure == S.Failure, Output == S.Input {
  <#code#>
}

subscriber附加到此publisher时将调用此方法,这就是我们需要发送此subscriber值的地方。然而,我们实际上不想在这里做任何自定义工作。我们只是想充当publishers的包装器,就像AnyPublisher一样。

所以让我们把AnyPublisher隐藏起来并委托给它:

let publisher: AnyPublisher<Output, Failure>

public func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
  self.publisher.receive(subscriber: subscriber)
}

现在Effect publisherAnyPublisher并没有什么不同,只不过它的失败是专门针对Never的。然而,有了自己的类型,我们就能更好地控制它的转变,这一点我们很快就会看到。

这造成了许多编译错误,第一个是:

effect.run(self.send)

🛑 Value of type ‘Effect’ has no member ‘run’

代替运行我们的effect,我们需要在它上调用sink,并且我们仍然可以将send方法传递给receive块:

effect.sink(receiveValue: self.send)

Now we have a warning:

⚠️ Result of call to ‘sink(receiveValue:)’ is unused

从技术上讲,我们的store中已经有了一个可取消对象的实例变量:

private var cancellable: Cancellable?

当我们讨论查看store的概念时,我们在关于模块化的章节中添加了这一点。我们展示了将操作全局状态和操作的store转换为操作本地状态和操作的store是可能的。但是,为了在本地store中反映对全局store的更改,我们必须订阅全局store并在本地store中重播这些更改。

所以我们不想重新使用这个可取消的值,它现在有一个非常重要的责任。事实上,也许我们应该重新命名它,以更好地反映它的目的:

private var viewCancellable: Cancellable?

我们需要分别跟踪这些可取消的效果和这个可取消视图。我们可以引入一个新的实例变量:

private var effectCancellable: Cancellable?

但我们真的需要一堆可取消的东西。每个reducer都可以返回一个effects数组,因此我们每次发送一个动作时都可能要处理多个effects。那么,让我们升级到一个数组:

private var effectCancellables: [Cancellable] = []

现在我们只需要保留调用sink时的可取消函数,这样它就能活很久,让我们的effect完成它的工作:

effectCancellables.append(
  effect.sink(receiveValue: self.send)
)

然而,这并不完全正确。这个effectCancellables数组将随着应用程序的进展而继续增长。我们永远不会从这个数组中删除可取消项。因此,我们需要一些方法来知道什么时候效果结束,然后我们应该从数组中删除它的可取消。

我们可以使用另一种超载水槽,让我们能够利用publisher的完成事件:

effectCancellables.append(
  effect.sink(
    receiveCompletion: { _ in
      <#code#>
    },
    receiveValue: self.send
  )
)

在这个receiveCompletion中,我们可以尝试从数组中移除这个cancellable,但我们实际上没有访问它的权限。我们遇到了一个先有鸡还是先有蛋的问题,cancellable是通过调用sink创建的,但我们需要从定义sink的一个闭包中访问cancellable

为了解决这个问题,我们需要将cancellable提取到隐式解包装的可选对象中,这允许我们在类型保存值之前获取变量,然后再对变量进行赋值。

它看起来像这样;

var effectCancellable: Cancellable!
effectCancellable = effect.sink(
  receiveCompletion: { _ in

},
  receiveValue: self.send
)
self.effectCancellables.append(
)

现在我们可以访问receiveCompletion内部的可取消数组,因此我们可以尝试在publisher完成时删除可取消对象。有许多方法可以从数组中删除一个值,例如删除第一个或最后一个或特定的下标,但要从数组中删除一个特定的值,我们必须搜索整个数组并找到我们想要删除的值:

self.effectCancellables.removeAll(where: { $0 == effectCancellable })

但这是行不通的,因为Cancellable协议没有继承Equatable协议,所以我们不能做这个等式检查。然而,AnyCancellable包装器是一个类,因此由于对象标识,它是equatable

我们把所有都升级到AnyCancellable。属性:

private var effectCancellables: [AnyCancellable] = []

// …
func send(_ action: Action) {
  // …
  var effectCancellable: AnyCancellable!
  effectCancellable = effect.sink(
    receiveCompletion: { _ in
      self.effectCancellables.removeAll(where: { $0 == effectCancellable })
  },
    receiveValue: self.send
  )

最后将这个cancellable添加到数组中。

self.effectCancellables.append(effectCancellable)

一个小问题是,我们在cancellablecompletion handler中引用了self,而cancellableself引用:

effectCancellable = effect.sink(
  receiveCompletion: { _ in
    self.effectCancellables.removeAll(where: { $0 == effectCancellable })
},

这意味着我们有一个循环引用,我们可以通过使用[**weak self]**来打破这个循环:

receiveCompletion: { [weak self] _ in
  self?.effectCancellables.removeAll(where: { $0 == effectCancellable })
}

这在技术上修复了我们架构的这一部分。然而,我们可以对此做出快速改进。AnyCancellable类符合Hashable协议,这意味着我们可以在一个集合中使用它们,这将给我们一个非常简单的方法来删除它们:

private var effectCancellables: Set<AnyCancellable> = []

// …
func send(_ action: Action) {
  // …
  var effectCancellable: AnyCancellable!
  effectCancellable = effect.sink(
    receiveCompletion: { [weak self] _ in
      self?.effectCancellables.remove(effectCancellable)
    },
    receiveValue: self.send
  )
  effectCancellables.insert(effectCancellable)

Pulling back reducers with publishers

现在,我们为我们的架构引入了自己的publisher Effect。我们已经升级了Store,在它的send方法中处理这些effects,包括处理它们相关的可取消和完成的所有复杂性。

下一个错误是在pullback方法中,让我们提醒自己这是做什么的。它需要reducers对局部state和行动起作用,并将它们拉回对更全局的state和行动的作用。当局部reducer产生局部effect时,我们需要将其转化为更全局的effect。局部effect可以将局部操作返回到store中,因此我们需要将该局部操作包装到更全局的操作中。

所以让我们看看使用我们的新publisher是什么样子的。

pullback函数当前有一个编译错误。

return localEffects.map { localEffect in
  Effect { callback in
    localEffect.run { localAction in // 🛑
      var globalAction = globalAction
      globalAction[keyPath: action] = localAction
      callback(globalAction)
    }
  }
}

🛑 Value of type ‘Effect’ has no member ‘run’

我们正在尝试调用run方法,该方法存在于我们的旧effect类型中,但在我们的publisher中不再存在。从技术上讲,我们可以在这里调用sink:

return localEffects.map { localEffect in
  Effect { callback in
    localEffect.sink { localAction in // 🛑
      var globalAction = globalAction
      globalAction[keyPath: action] = localAction
      callback(globalAction)
    }
  }
}

sink返回一个AnyCancellable,这是我们需要跟踪的东西,我们甚至不清楚我们要怎么做,因为我们在一个pullback,这是在纯函数reducer的世界,没有store在可视范围内管理这些细节。

我们还试图创建一个带有回调闭包的效果,但我们不再拥有那个接口。

如果我们想一下这段代码实际上在做什么,它只是试图将一个可以产生局部动作的effect转换成一个可以产生全局动作的effect。这正是map操作允许我们对泛型类型所做的,幸运的是,Publisher类型支持map操作!

我们可以利用Publisher拥有map这一事实,将所有这些手动effect转换代码替换为一个简单的map:

localEffect.map { localAction in
  var globalAction = globalAction
  globalAction[keyPath: action] = localAction
  callback(globalAction)
}

然而,我们现在可以简单地将这个值返回给map,而不是将globalAction提供给callback函数:

localEffect.map { localAction in
  var globalAction = globalAction
  globalAction[keyPath: action] = localAction
  return globalAction
}

这基本上是对的,但不是编译。这个错误消息不是很好,但是如果我们在闭包中添加一个返回类型,它会变得好一点:

localEffect.map { localAction -> GlobalAction in
  var globalAction = globalAction
  globalAction[keyPath: action] = localAction
  return globalAction
}

🛑 Cannot convert value of type ‘Publishers.Map<Effect, GlobalAction>’ to closure result type ‘Effect’

这是一个很长的错误消息,但它正确地描述了什么是错误的。我们期望从map返回的类型是Effect,因为我们需要从这个函数返回这些东西的数组。但它没有找到这种类型,而是找到了这个奇怪的**Publisher.Map<Effect, GlobalAction>**类型,有点拗口。

这给我们带来了在处理Combine框架时的另一个重要教训。 Publisher有很多操作,像map, zip, flatMap, filter等等。但它们不会返回与它们所操作的publisher类型完全相同的publisher,它们只返回符合Publisher协议的内容。这是由于Swift的类型系统的限制,虽然功能强大,但不能表达对一个publisher进行map操作返回一个相同类型的publisher的想法,比如map on Effect返回另一个Effect

map操作返回的类型称为Map publisher

Publishers.Map<Effect<LocalAction>, GlobalAction>

第一个泛型指的是我们要映射的publisher,第二个泛型指的是映射的publisher可以发出的新值。这表示我们已经映射到Effect上将LocalActions的值转换为GlobalActions

我们需要把这个publisher变成一个普通的旧Effect

这与CombineAnyPublisher所面临的问题完全相同,他们的解决方法是允许任何publisher将自己转变为AnyPublisher:

.eraseToAnyPublisher()

我们基本上想要这个功能,但用我们的Effect类型代替。可以创建一个eraseToEffect方法。它被表示为Publisher协议上的扩展,因此任何publisher都可以被删除,但它应该只在不能失败的publisher上工作:

extension Publisher where Failure == Never {
  public func eraseToEffect() -> Effect<Output> {

  }
}

要实现这个方法,我们只需要将自己擦除到AnyPublisher,然后将其包装在effect类型中:

extension Publisher where Failure == Never {
  public func eraseToEffect() -> Effect<Output> {
    return Effect(publisher: self.eraseToAnyPublisher())
  }
}

这就是它的全部内容,但是在橡皮类型中做这么多包装似乎很奇怪。我们这样做不是没有理由的,它很快就会很方便,但现在就相信我们的话吧。

回到我们的pullback操作,我们现在可以将Map发布者删除为Effect发布者

localEffect.map { localAction -> GlobalAction in
  var globalAction = globalAction
  globalAction[keyPath: action] = localAction
  return globalAction
}
.eraseToEffect()

这最终使编译器高兴。

不幸的是,当涉及到处理Combine时,这种eraseToEffect dance将是常见的做法。
因为我们的reducer是用Effect来定义的,而当你转换它的时候,你得到的不是一个Effect,所以在返回效果之前,我们需要多次调用eraseToEffect。这很烦人,但很有必要。

不过,publishers拥有map操作符是件很酷的事。我们第一次谈论地图是在Point-Free的第13集,似乎是很久以前的事了! 但在那一集中,我们展示了map操作是一个非常普遍的东西,当涉及到定义它时,没有很多选择。为了支持他们,我们提到了Swift标准库有两个映射:一个定义在数组上,另一个定义在可选项上。从那时起,引入了Result类型,并且它还具有map操作符。现在我们甚至在Combine框架中有了一个map操作符。这意味着仅在苹果的生态系统中就有4种不同的地图操作!

Finishing the architecture refactor

这个文件中的下一个错误是在我们的日志高阶reducer中。这里我们试图返回一个包装了一些打印语句的效果。

return [Effect { _ in // 🛑
  print("Action: \(action)")
  print("Value:")
  dump(newValue)
  print("---")
}] + effects

🛑 Cannot convert value of type ‘() -> ()’ to expected argument type ’AnyPublisher<, Never>’

我们在Effect上不再有基于回调闭包的初始化式,但我们以前在Combine世界中遇到过类似的解决方案。

我们希望返回一个publisher,它封装传入的工作的执行,但我们希望确保在订阅publisher之前不运行该工作。正如我们之前看到的,一个简单的方法是将它包装在一个Deferred publisher中:

return [Deferred { _ in // 🛑
  print("Action: \(action)")
  print("Value:")
  dump(newValue)
  print("---")
}] + effects

🛑 Contextual closure type ‘() -> _’ expects 0 arguments, but 1 was used in closure body

现在Deferred要求我们返回一个发布者,但实际上我们不想做任何事情,因为这是一个“fire-and-forget”效应。幸运的是,有一个名为Empty的特殊发布者,它从不发出任何值,我们甚至可以立即完成它:

return [Deferred { _ in
  print("Action: \(action)")
  print("Value:")
  dump(newValue)
  print("---")
  return Empty(completeImmediately: true)
}] + effects

这不能编译,因为Swift需要一些类型推断方面的帮助:

return [Deferred { () -> Empty<Action, Never> in
  print("Action: \(action)")
  print("Value:")
  dump(newValue)
  print("---")
  return Empty(completeImmediately: true)
}] + effects

现在我们得到了一个更好的错误消息。

🛑 Cannot convert value of type ‘Deferred<Empty<Action, Never>>’ to expected element type ‘Effect’

所以我们需要将它擦除到Effect类型:

return [Deferred { () -> Empty<Action, Never> in
  print("Action: \(action)")
  print("Value:")
  dump(newValue)
  print("---")
  return Empty(completeImmediately: true)
}.eraseToEffect()] + effects

编译器很高兴,但我们需要做的工作有点粗糙。让我们引入一个助手,它以一种更好的方式完成同样的工作。 我们将它创建为effect上的静态函数,接受一个void-to-void闭包并返回一个effect:

extension Effect {
  public static func fireAndForget(work: @escaping () -> Void) -> Effect {
  }
}

我们可以捕捉我们在帮助者身上做的工作。

extension Effect {
  public static func fireAndForget(work: @escaping () -> Void) -> Effect {
    return Deferred { () -> Empty<Output, Never> in
      work()
      return Empty(completeImmediately: true)
    }
    .eraseToEffect()
  }
}

现在这是完美的打印效果:

return [.fireAndForget {
  print("Action: \(action)")
  print("Value:")
  dump(newValue)
  print("---")
}] + effects

这就是我们引入自定义Effect publisher一致性的主要原因,而不是仅仅依赖AnyPublisher。这将给我们机会,让transforming publishers 在调用上变得更好。

可组合架构模块还没有编译,因为我们有这个上次提取出来的effects文件。 它包含一些方便的基本effects,用于网络请求、JSON解码和强制在特定队列上传递值。

所有这些效果都很棒,但是Combine框架实际上提供了完成所有这些任务的API。我们甚至用Combine框架的名称来命名方法。

所以我们实际上不需要修复这些编译错误,我们只是简单地注释掉所有东西,因为我们可以依靠Combine来实现这些效果,而不是自己重新创建它们。

现在可组合架构模块正在编译。

Refactoring synchronous effects

现在终于构建了ComposableArchitecture模块,我们可以开始重构应用程序,以使用这种新的副作用。我们有几个模块需要构建,所以让我们看看它是如何运行的。

让我们一个一个地应对他们。

我们可以从最简单的PrimeModal开始。如果我们切换到那个目标并进行构建,我们会惊奇地发现一切都还在构建。 这是因为primeModalReducer不会产生任何副作用,因此它并不关心我们是否更改了effect类型的定义。

下一个最简单的模块是FavoritePrimes模块,它无法编译,因为我们构造了一些保存和加载最爱质数的effects

saveEffect是一个fire-and-forget效果,它目前使用的是旧的回调风格的effect。我们可以简单地将它替换为新的fireAndForget助手来编译这个部分:

private func saveEffect(favoritePrimes: [Int]) -> Effect<FavoritePrimesAction> {
  return .fireAndForget {
    ...
  }
}

loadEffect是一个同步效果,需要将结果反馈给系统,并且它目前也在使用旧的回调风格的效果。类似于在Effect上创建fireAndForget helper,我们可以创建一个同步效果helper

它只是一个函数,它执行一些生成动作的工作,我们想返回一个包装该工作的effect

extension Effect {
  public static func sync(work: @escaping () -> Output) -> Effect {
  }
}

同样,我们希望将工作包装在Deferred中,以便在订阅完成之前不执行工作:

extension Effect {
  public static func sync(work: @escaping () -> Output) -> Effect {
    return Deferred {
    }
  }
}

然后在这里,我们想返回一个保存作品结果的publisher。幸运的是,Combine还为我们提供了另一个具体的publisher,它的名称是Just:

extension Effect {
  public static func sync(work: @escaping () -> Output) -> Effect {
    return Deferred {
      Just(work())
    }
  }
}

当然,我们总是需要将它擦除到Effect类型:

extension Effect {
  public static func sync(work: @escaping () -> Output?) -> Effect {
    return Deferred {
      Just(work())
    }
    .eraseToEffect()
  }
}

这最终让我们进入了编译顺序。现在我们可以用它来定义我们的loadEffect:

private let loadEffect = Effect<FavoritePrimesAction>.sync {
  ...
    else { return }
  return .loadedFavoritePrimes(favoritePrimes)
}

这不是很正确,因为效果<FavoritePrimesAction>.sync闭包必须返回一个实际的FavoritePrimesAction,但是这里我们返回一个可选的。我们在guard中返回nil是因为某些东西失败了,我们只是不想在那种情况下产生effect。我们可以做的一件事是将其更改为Effect<FavoritePrimesAction?>

private let loadEffect = Effect<FavoritePrimesAction?>.sync {
  ...
    else { return nil }
  return .loadedFavoritePrimes(favoritePrimes)
}

但是我们的reducer还不能直接使用loadEffect,因为它需要与返回非可选动作的effects一起工作。为了摆脱nil,我们可以使用compactMap:

return [
  loadEffect
    .compactMap { $0 }
}

最后,我们需要重新擦除。

.compactMap { $0 }
.eraseToEffect()

这个compactMap操作符在publishers上的工作就像它在数组上的工作一样,它只是简单地过滤掉nil,只留下诚实的值。

就像favorite素数模块正在构建的那样。不幸的是,我们的整个应用还没有构建,但幸运的是,由于我们在模块化这个应用的努力,我们有一个playground,允许我们在构建其他东西之前,完全独立地运行这个屏幕。我们跳过去转一圈吧。

我们可以删除最喜欢的prime并保存更改…

Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value: file PrimeTime/ComposableArchitecture/ComposableArchitecture.swift, line 80

哎哟!看来我们的可组合架构模块崩溃了。我们一定忽略了什么。如果我们到第53行,我们会看到我们在这一行崩溃了:

self?.effectCancellables.remove(effectCancellable)

这只会在effectCancellablenil时崩溃,因为我们使用的是隐式解包装可选选项,这总是意味着无论我们多么小心,都有崩溃的可能性。

这里的问题是,我们假设sink将在调用这些闭包之前返回,以便我们能够获得cancellable。事实证明,对于立即完成的publishers来说,在sink返回之前,闭包就会被立即调用,这就是我们崩溃的原因。 我们可以做的一件事是将effectCancellable设置为可选的,这样我们就可以安全地处理它:

var effectCancellable: AnyCancellable?
effectCancellable = effect.sink(
  receiveCompletion: { [weak self] _ in
    guard let effectCancellable = effectCancellable else { return }
    self?.effectCancellables.remove(effectCancellable)
  },
  receiveValue: self.send
)
if let effectCancellable = effectCancellable {
  effectCancellables.insert(effectCancellable)
}

现在我们只需要检查effectCancellable是否存在,然后将其从集合中移除,再将其插入集合中。然而,这是不正确的。如果publisher立即完成,它将被插入到集合中,但我们永远没有机会删除它,因为receiveCompletion在我们插入之前就已经执行了。

因此,我们需要能够理解,当我们到达插入行时,receiveCompletion闭包是否已经被调用,以便我们可以跳过它。为了做到这一点,我们还需要另一个可变变量来跟踪该状态:

var effectCancellable: AnyCancellable?
var didComplete = false
effectCancellable = effect.sink(
  receiveCompletion: { [weak self] _ in
    didComplete = true
    guard let effectCancellable = effectCancellable else { return }
    self?.effectCancellables.remove(effectCancellable)
  },
  receiveValue: self.send
)
if !didComplete, let effectCancellable = effectCancellable {
  effectCancellables.insert(effectCancellable)
}

现在我们只会在publisher没有立即完成的情况下将可取消的内容插入到集合中。如果我们跳回到我们的playground,我们会看到现在一切都恢复正常了。

Refactoring asynchronous effects

好的,两个favorite素数模块的效果现在完全由combine提供。

我们甚至得到了几个助手,包括fireAndForget和sync

看到我们的模块化努力得到了回报也很不错:我们不必为了单独运行这个屏幕而构建整个应用程序。这就是模块化的力量!我们可以自由地进行广泛的增量重构,并在此过程中为每个屏幕提供反馈。

不过,我们还有一个屏幕:Counter模块,它具有复杂的异步效果,可以发出API请求。让我们看看要将其升级到Combine需要改变什么。

我们将看到的第一个错误是在wolframAlpha函数中,它目前正在使用我们上次构建的一些效果助手,比如这个dataTask和decode助手:

return dataTask(with: components.url(relativeTo: nil)!)
  .decode(as: WolframAlphaResult.self)

幸运的是,Combine为我们提供了所有这些帮助,但不幸的是,使用它们也需要更多的工作。我们可以使用FoundationURLSession上的新dataTaskPublisher方法来获取一个表示网络请求的publisher :

URLSession.shared
  .dataTaskPublisher(for: components.url(relativeTo: nil)!)

让我们把它赋值给一个临时变量,这样我们就可以了解它的类型是什么样的:

let tmp = URLSession.shared
  .dataTaskPublisher(for: components.url(relativeTo: nil)!)

其类型为URLSession.DataTaskPublisher,它是Foundation中定义的具体publisher。如果我们检查它的Output和Failure,我们将发现以下类型别名。

typealias Output = (data: Data, response: URLResponse)
// …
typealias Failure = URLError

记住,最终我们需要从这个方法返回一个Effect,所以如果我们用eraseToAnyPublisher方法擦除这个,我们将得到一个更好的实际类型视图:

let tmp = URLSession.shared
  .dataTaskPublisher(for: components.url(relativeTo: nil)!)
  .eraseToAnyPublisher()
// AnyPublisher<URLSession.DataTaskPublisher.Output, URLSession.DataTaskPublisher.Failure>

我们希望将这些数据解码到我们的WolframAlphaResult模型中,幸运的是,Combine提供了一个很好的解码助手:

let tmp = URLSession.shared
  .dataTaskPublisher(for: components.url(relativeTo: nil)!)
  .decode(type: WolframAlphaResult.self, decoder: JSONDecoder())
  .eraseToAnyPublisher()

然而,这将无法编译,因为.decode方法只在输出与我们试图解码的内容匹配的publishers上定义,在本例中是Data。 因此,在调用decode之前,我们必须映射到publisher,从元组中提取数据:

let tmp = URLSession.shared
  .dataTaskPublisher(for: components.url(relativeTo: nil)!)
  .map { data, _ in data }
  .decode(type: WolframAlphaResult.self, decoder: JSONDecoder())
  .eraseToAnyPublisher()

现在我们的publisher类型是:

// AnyPublisher<WolframAlphaResult, Publishers.Decode<Upstream, Output, Coder>.Failure>

快到了。这里的主要问题是,我们的publisher可以出错,但我们的effect必须返回错误类型为Neverpublisher,这意味着它永远不会出错。将一个可能出错的publisher转换成永远不会出错的publisher最简单的方法是使用replaceError方法,它允许你简单地用输出值替换任何发生的错误:

.replaceError(with: <#T##WolframAlphaResult#>)

这里的问题是,我们需要用诚实的WolframAlphaResult替换错误,但我们没有这样的值。我们宁愿使用nil来表示我们不能构造一个WolframAlphaResult,但是为了达到这个目的,我们的发布者需要有一个可选的WolframAlphaResult的输出。

最简单的方法是解码一个可选的结果,并将错误替换为nil:

.decode(type: WolframAlphaResult?.self, decoder: JSONDecoder())
.replaceError(with: nil)

最后,我们的转换链给了我们一个类型的publisher:

AnyPublisher<WolframAlphaResult?, Publishers.ReplaceError<Upstream>.Failure>

这个错误很难读取,但是如果我们按照它的定义来执行,它就是Never,这正是我们需要它来擦除一个Effect并返回它的地方。

return URLSession.shared
  .dataTaskPublisher(for: components.url(relativeTo: nil)!)
  .map { data, _ in data }
  .decode(type: WolframAlphaResult.self, decoder: JSONDecoder())
  .map(Optional.some)
  .replaceError(with: nil)
  .eraseToEffect()

现在这种effect正在构建。虽然很紧张,但至少Combine框架为我们提供了完成工作的所有工具。

下一个错误是在nthPrime中,它调用了wolframAlpha效果,但随后映射到它。正如我们之前看到的,映射到一个publisher会改变它的类型,所以我们必须删除这个改变:

.eraseToEffect()

现在这个函数高兴了。剩下的唯一编译错误是在计数器减数器中使用这种effect的地方。因为我们正在map这个publisher,所以我们再次使用了eraseToEffect:

nthPrime(state.count)
  .map(CounterAction.nthPrimeResponse)
  .receive(on: .main)
  .eraseToEffect()

然而,事情似乎仍然没有进展。错误信息不是很好,但问题在这里:

.receive(on: .main)

receive(on:)方法接受Scheduler:

.receive(on: <#T##Scheduler#>)

这是一种协议,不是具体类型。DispatchQueues和RunLoops都符合这个协议,所以我们只需要更明确地使用它的类型:

.receive(on: DispatchQueue.main)

现在这个模块终于开始构建了。更好的是,整个应用目标正在构建,这意味着我们可以再次运行应用。如果我们试一试,我们将看到一切仍然一样的工作,但现在它运行的Combine而不是我们自己的自定义effect类型。我不知道你是怎么想的,但是当我知道Combine正在驱动它的时候,这个应用感觉更好了😄。

What’s the point?

至此,我们已经完成了过去几周构建的这个玩具应用程序的Combine框架重构。一旦我们了解了Combine是如何工作的,这就非常简单了。

在point - free上,我们喜欢在每一集的结尾都问“这有什么意义?!”,现在是我们把事情弄清楚并讨论为什么这些想法很重要的时候了。然而,这一集从一开始就相当实用。我们注意到Effect类型看起来与许多响应式编程库非常相似,比如ReactiveSwift、RxSwift和最近来自苹果的Combine。所以,我们想也许我们可以依靠其中一个框架而不是滚动我们自己的类型,确实我们可以。Effect类型最重要的特性是它可以表示异步工作,并且它是可转换的。好吧,所有这些响应库也恰恰提供了这一点,所以不妨利用一下。

但也许这一集的“关键点”是,通过使用非常简单的、集中的类型来完成一件事情,并以可转换的方式进行,您以后就可以在未来接受许多优秀的重构。事实上,我们能够将我们的effects类型完全转换为另一种类型,一切都继续工作,这是非常令人惊讶的。事实上,你可能会问,为什么我们不能抽象出effects的形状,这样库的用户就可以带来他们自己的effects类型?也许他们想使用ReactiveSwift或RxSwift而不是Combine,也许他们只想滚动自己的简单类型。

不幸的是,Swift的类型系统不够强大,无法表达这种想法,但至少我们可以看到这种抽象的形状,并试图将其作为我们如何构建库的灵感。

这一集就到这里。在构建可组合架构的章节中,我们稍微绕了一下,以便解决上节课提到的Effect类型的奇怪之处。但下周我们将继续上次的话题:testing!我们想要展示这个架构的可测试性,即使涉及到效果。