【Swift】throwするエラーの型を指定する方法

## はじめに
Swift6から`Typed throw`がサポートされ、throwするエラーの型を指定できるようになりました。そこで今回は`Typed throw`の基本的な使い方と、使うべき場面について調べたことをまとめます。

## 基本的な使い方
エラーを返す関数に`throws(エラーの型)`のようにすることで、スローするエラーの型を指定することができます。
```Swift
enum SampleError: Error {
    case invalidData
}

func errorFunction() throws(SampleError) {
    throw .invalidData
}
```

この`errorFunction`は常に`SampleError`をスローすることが保証されています。そのため`do-catch`でキャッチできるエラーはSampleErrorになります。

```Swift
do {
    try errorFunction()
} catch {
    print(error) // SampleError
}
```

`SampleError`をスローされるとは保障されていない関数とともに呼び出すと、キャッチされるエラーは`any Error`になります。

```Swift
enum SampleError: Error {
    case invalidData
}

func errorFunction() throws(SampleError) {
    throw .invalidData
}

func anyErrorFunction() throws {
    throw SampleError.invalidData
}

do {
    try errorFunction()
    try anyErrorFunction()
} catch {
    print(error) // any Error
}
```

ちなみに既存のthrowの書き方は、`any Error`型のTyped throwと同じです。
```Swift
// この2つは同じ
func hoge() throws {...}
func hoge() throws(any Error) {...}
```
またthrowしない関数は、`Never`のTyped throwと同じです。
```Swift
func hoge() {...}
func hoge() throws(Never) {...}
```

## Typed throwは変更に弱い
Typed throwは一見すると便利な機能ですが、限定的な場面以外では基本的に推奨されていません。Typed throwのプロポーザル[^1]にも以下のように記載されています。
> The error values themselves are always type-erased to any Error. This approach encourages errors to be handled generically, and remains a good default for most code.（エラー値自体は、常に任意のErrorに型消去されます。 このアプローチは、エラーを一般的に扱うことを推奨しており、ほとんどのコードにとって良いデフォルトであることに変わりはない。）

その理由はTyped throwは変更に弱いからです。

例えば以下のような関数があるとします。
```Swift
func performTask() throws(MyError) { 
    throw MyError.networkFailure
}
```
のちに新しいエラー型をスローする必要に迫られた場合、関数のシグネチャを変更しなければなりません。シグネチャを変更するということは、この関数の呼び出し元すべてに影響を与えるということです。

逆にTyped throwを使わなかった場合、
```Swift
func performTask() throws { 
    throw MyError.networkFailure
}
```
のちに新たなエラー型をスローすることになっても、関数内部のコードを変更するだけで済みます。

このような背景があり、ほとんどのケースではTyped throwを使うべきではありません。

## Typed throwを使うべき場面
ではTyped throwを使うべき場面はというと、プロポーザルに記載されています。
- エラーハンドリングが、モジュールやパッケージに閉じていること
- rethrows や Result のようにそれ自体がエラーを生み出さず、ただ通過するだけの場合
- オーバーヘッドが許容できない場合

1つ目はシンプルに、エラーがモジュール内で完結していて、外部に公開されていないパターンです。
3つ目は詳しくわかりませんが、容量的にTyped throwを使った方がいいケース？

個人的に2つ目の言っていることがわからなかったので、少し深掘りしてみました。

## rethrowsをTyped throwに置き換える
`rethrow`には**引数として受け取ったクロージャ以外のところでエラーをスローできない**という制約があります。

```Swift
func countMatching(_ numbers: [Int], predicate: (Int) throws -> Bool) rethrows -> Int {
    var count = 0
    var caughtError: Error?

    for number in numbers {
        do {
            if try predicate(number) {
                count += 1
            }
        } catch {
            caughtError = error
            break
        }
    }

    if let error = caughtError {
        throw error // ⭐️A function declared 'rethrows' may only throw if its parameter does
    }

    return count
}
```
このコードではクロージャとは別の場所でエラーをスローしているため、ビルドエラーが発生します。

そこでTyped throwとジェネリクスをうまく使えば、この問題を解決することができます。
```Swift
func countMatching<E: Error>(_ numbers: [Int], predicate: (Int) throws(E) -> Bool) throws(E) -> Int {
    var count = 0
    var caughtError: E?

    for number in numbers {
        do {
            if try predicate(number) {
                count += 1
            }
        } catch {
            caughtError = error
            break
        }
    }

    if let error = caughtError {
        throw error
    }

    return count
}
```
このコードは問題なく動作します。

このようにその関数自体がエラーを発生させるのではなく、ただエラーを伝播させる場合はTyped throwが適しています。

ちなみに`map`や`count(where:)`もTyped throwで実装されています。
```Swift
public func map<T, E>(_ transform: (Element) throws(E) -> T) throws(E) -> [T] where E : Error
```

```Swift
public func count<E>(where predicate: (Element) throws(E) -> Bool) throws(E) -> Int where E : Error
```


## 参考にした記事
- https://speakerdeck.com/koher/swift-6notyped-throwstoswiftniokeruerahandoringunoquan-ti-xiang-woxue-bu
- https://note.amazia.co.jp/n/n31f131be06d7
- https://speakerdeck.com/toshiyana36/swift6karanotyped-throws

[^1]:https://github.com/swiftlang/swift-evolution/blob/main/proposals/0413-typed-throws.md#introduction