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用于publisher的Failure类型:
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 publisher和AnyPublisher并没有什么不同,只不过它的失败是专门针对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)
一个小问题是,我们在cancellable的completion handler中引用了self,而cancellable被self引用:
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
这给我们带来了在处理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。
这与Combine的AnyPublisher所面临的问题完全相同,他们的解决方法是允许任何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)
这只会在effectCancellable为nil时崩溃,因为我们使用的是隐式解包装可选选项,这总是意味着无论我们多么小心,都有崩溃的可能性。
这里的问题是,我们假设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为我们提供了所有这些帮助,但不幸的是,使用它们也需要更多的工作。我们可以使用Foundation在URLSession上的新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必须返回错误类型为Never的publisher,这意味着它永远不会出错。将一个可能出错的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!我们想要展示这个架构的可测试性,即使涉及到效果。