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

現在のTCAではTaskResultのGenericパラメータである`Success`がVoidの場合にEquatableに準拠することができません。それはVoidがEquatableではないためです。
なので、この問題の解決法をいくつか紹介します。
また、この問題について公式の見解が得られたので、そちらの紹介もしていきます。

# 方法1
まず一つ目の方法は`Equatable`に準拠した空の構造体を作成する方法です。

```Swift
public struct EquatableVoid: Equatable {
    public init() {}
}
```
そしてこのように使用することができます。
```Swift
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を送り、提案をした案です。
コードが少し長いのでこちらを見ていただけたらと思います。

https://github.com/pointfreeco/swift-composable-architecture/pull/2375/files

このPRでは、VoidをEquatableに準拠するためだけに`TaskVoidResult`という型を作成したものとなっております。

Swiftではそもそもこのような書き方ができません。

```Swift
extension TaskResult: Equatable where Success: Equatable {}
extension TaskResult: Equatable where Success == Void {}
```

この書き方は以前はできたようですが、こちらに記載されている通りすでにSwiftではbanされているとのことです。

https://github.com/apple/swift-evolution/blob/master/proposals/0143-conditional-conformances.md#overlapping-conformances

このようにwhereを使って複数の条件でEqutableに準拠させることができないので、TaskVoidResultという型を作ってこの問題を回避しました。

またPRでは、方法1のやり方だとEquatableVoidのインスタンスが余計に生成されてしまい、ARCの呼び出しが増えてパフォーマンスが若干悪くなると説明しました。ですが実際にベンチマークをとった結果、ほぼほぼ速度が変わらないという結果になりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627061/42e73795-f823-0926-e366-7209e072e924.png)

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

# 公式の見解
そこでTCAの開発者であるstephenceilsさんはこのような解決策を示しました。

```Swift
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を作成してます。

このように使用します。
```Swift
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`を作成してもいいんじゃない？」と言われたため、この問題を解決したライブラリを公開しました！

https://github.com/Ryu0118/swift-composable-architecture-extras

機能は1つしかないですが、これからどんどん足していければなと思っております。
ちなみにTCAのREADMEにこちらのライブラリへのリンクを載せていただきました🙏

![スクリーンショット 2023-08-23 2.56.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2627061/9ac6aaaf-d927-679b-79a1-7c3e70727f07.png)

# さいごに
なぜVoidはEquatableではないのか...