LoginSignup
14
3

[TCA] TaskResult<Void>がEquatableに準拠できない問題の解決と公式の見解

Posted at

現在の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の呼び出しが増えてパフォーマンスが若干悪くなると説明しました。ですが実際にベンチマークをとった結果、ほぼほぼ速度が変わらないという結果になりました。

image.png

なので、こちらのパフォーマンスの差は気にする必要がないようです。

公式の見解

そこで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にこちらのライブラリへのリンクを載せていただきました🙏

スクリーンショット 2023-08-23 2.56.51.png

さいごに

なぜVoidはEquatableではないのか...

14
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
3