現在のTCAではTaskResultのGenericパラメータであるSuccess
がVoidの場合にEquatableに準拠することができません。それはVoidがEquatableではないためです。
なので、この問題の解決法をいくつか紹介します。
また、この問題について公式の見解が得られたので、そちらの紹介もしていきます。
方法1
まず一つ目の方法はEquatable
に準拠した空の構造体を作成する方法です。
public struct EquatableVoid: Equatable {
public init() {}
}
そしてこのように使用することができます。
public struct HogeFeature: Reducer {
public struct State: Equatable {}
public enum Action: Equatable {
case onAppear
case response(TaskResult<EquatableVoid>)
}
public func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .onAppear:
return .run { send in
await send(
.response(
TaskResult {
try await asyncThrowsVoidFunc()
return EquatableVoid()
}
)
)
}
case .response(.success):
// handle error
return .none
case .response(.failure(let error)):
// handle error
return .none
}
}
}
方法2
この方法は、私がTCAにPRを送り、提案をした案です。
コードが少し長いのでこちらを見ていただけたらと思います。
このPRでは、VoidをEquatableに準拠するためだけにTaskVoidResult
という型を作成したものとなっております。
Swiftではそもそもこのような書き方ができません。
extension TaskResult: Equatable where Success: Equatable {}
extension TaskResult: Equatable where Success == Void {}
この書き方は以前はできたようですが、こちらに記載されている通りすでにSwiftではbanされているとのことです。
このようにwhereを使って複数の条件でEqutableに準拠させることができないので、TaskVoidResultという型を作ってこの問題を回避しました。
またPRでは、方法1のやり方だとEquatableVoidのインスタンスが余計に生成されてしまい、ARCの呼び出しが増えてパフォーマンスが若干悪くなると説明しました。ですが実際にベンチマークをとった結果、ほぼほぼ速度が変わらないという結果になりました。
なので、こちらのパフォーマンスの差は気にする必要がないようです。
公式の見解
そこでTCAの開発者であるstephenceilsさんはこのような解決策を示しました。
public struct VoidSuccess: Codable, Sendable, Hashable {
public init() {}
}
extension TaskResult where Success == VoidSuccess {
public init(catching body: @Sendable () async throws -> Void) async {
do {
try await body()
self = .success(VoidSuccess())
} catch {
self = .failure(error)
}
}
}
これは完璧な解決策ですね。。
VoidSuccess
という型を作成し、TaskResultのextensionにクロージャの戻り値がVoidの時のinitializerを作成してます。
このように使用します。
public struct HogeFeature: Reducer {
public struct State: Equatable {}
public enum Action: Equatable {
case onAppear
case response(TaskResult<VoidSuccess>)
}
public func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action {
case .onAppear:
return .run { send in
await send(
.response(
TaskResult { try await asyncThrowsVoidFunc() }
)
)
}
case .response(.success):
return .none
case .response(.failure(let error)):
// handle error
return .none
}
}
}
これで戻り値がVoidの関数の時にいちいち return EquatableVoid()
と書いていた問題が削除できましたね!
そして、stephenceilsさんから、「swift-composable-architecture-extras
を作成してもいいんじゃない?」と言われたため、この問題を解決したライブラリを公開しました!
機能は1つしかないですが、これからどんどん足していければなと思っております。
ちなみにTCAのREADMEにこちらのライブラリへのリンクを載せていただきました🙏
さいごに
なぜVoidはEquatableではないのか...