I was working in TCA and wondered where async/await could fit in...my first thought was to add a convenience init to the Reducer that is async like this:
But this has the unfortunate consequence of needing to use two separate closures, one for syncronous state changes and the second for asynchronous effects. Switching twice seems a bit cumbersome. Next I tried creating an easy way to form effects from async code:
Another function could be created to use AsyncSequence when many results are expected. (I defined this as a global function because there seemed to be a bug inferring async { ... } inside an extension on Effect. It was confusing it for a static func on a publisher?). In practice this looks like:
case .fetchAccounts:
state.loading = true
return effect(scheduler: environment.mainQueue) { [state] in
do {
let accounts = try await environment.accountsService.fetchAccounts(state.user)
return .receivedAccounts(accounts)
} catch {
return .failedToReceiveAccounts(.requestFailed(error.localizedDescription))
}
}
These are pretty rough ideas but fun to work through. Has anyone else though through how async/await could fit into TCA?
State, in SwiftUI at least, is @MainActor-isolated (actually @MainActor(unsafe) for compatibility). So, I suggest marking state-handling code @MainActor-isolated. As to how you could incorporate async/await, I don’t think I have something better to offer compared to your first example.
That makes sense! I currently am not using the Reducer initializer since managing two separate closures that switch over the same enum feels overly cumbersome. But it would be nice to build that in somehow.
I have also added this function. I am definitely not sure about the name of it....but basically it takes a throwing async function, catches it to a result type, and then embeds that result into the provided Action case.
Note that I didn't include a scheduler as a parameter, as Swift should automatically handle creating detached tasks. Having said that, I'm not sure how the implementation should look like, without changing the internals of Effect to support Swift Concurrency out of the box.
Nice suggestions! I originally did not define as an initializer because of an odd bug where async { ... } (soon to be task) is mistaken for an ambiguous static function on Effect. I was able to get around this temporarily by doing the following:
FWIW we also just removed some old deprecated code, including that async function, so you shouldn't get that error anymore. Though it won't matter much once Task is properly in place