Result型を使ってエラーをスマートにハンドリングする方法(Swift)

## はじめに

2020/03/29現在、Swiftではスローするエラーの型を指定できません。
そのため、スローするエラーの型が決まっていたとしても、すべてのエラーをキャッチする必要があります。

`MyError` というエラーの型のみを返す `myFunc(isSuccess:)` メソッドを例に考えてみます。
（この例では `MyError.bar` は返ることがないのですが、返る可能性があると考えてください）

```swift
enum MyError: Error {
    case foo(_ message: String)
    case bar(_ message: String)
}

func myFunc(isSuccess: Bool) throws -> String {
    if isSuccess {
        return "Success!!"
    } else {
        throw MyError.foo("Foo")
    }
}
```

`myFunc(isSuccess:)` メソッドを呼び出します。
エラーの型を `MyError` だと決めていますが、一番下に `} catch {` を書いてすべてのエラーをキャッチする必要があります。

```swift:before
do {
    let result = try myFunc(isSuccess: false)
    // `result` を使う処理
} catch MyError.foo(let message) {
    // エラーハンドリング
} catch MyError.bar(let message) {
    // エラーハンドリング
} catch {
    // !!!: 絶対に入らないけど必要
}
```

もし一番下の `} catch {` を書いていないと、以下のビルドエラーが発生します。

```
Errors thrown from here are not handled because the enclosing catch is not exhaustive
ここからスローされたエラーは、囲んでいるキャッチが完全ではないため処理されません
```

Switch文で `default:` をできる限り書かないようにするのと同様、絶対に入らないとわかっているエラーに対してハンドリングしたくありません。
絶対に入らないことを明確にするため、 `fatalError(_:)` を使っている人も多いと思います。

```swift
} catch {
    fatalError("Unexpected error: \(error).")
}
```

いずれにせよスマートではないように感じます。
[Twitterでスマートな方法を教えていただいた](https://twitter.com/takasek/status/1242419938297405442?s=20)ので紹介します。

## 環境

- OS：macOS Mojave 10.14.6
- Swift：5.1.3
- Xcode：11.3.1 (11C504)

## 結論

先に結論を述べます。
`Result.init(catching:)` と `Result.mapError(_:)` を使って実現します。

```swift:after
let result = Result { try myFunc(isSuccess: false) }
    .mapError { $0 as! MyError }

switch result {
case .success(let string):
    // `string` を使う処理
case .failure(let error):
    switch error {
    case .foo(let message):
        // エラーハンドリング
    case .bar(let message):
        // エラーハンドリング
    }
}
```

こうすることで、 `MyError` 型のみをハンドリングできるようになりました。
エラーを列挙型で定義している場合、Switch文が使えるので、エラーのハンドリング漏れも防げます。

## 解説

おそらくSwiftに慣れていても、上記のような書き方を知らない人は多いと思います。
少なくとも私は知りませんでした。

1つずつ解説します。

### Result.init(catching:)

まずは1行目です。

```swift
let result = Result { try myFunc(isSuccess: false) }
```

こちらは `Result` 型のイニシャライザを、トレイリングクロージャを使って呼び出しています。

`Result` 型には `Result.init(catching:)` という、スローするクロージャを引数に持つイニシャライザがあります。
クロージャの処理が成功した場合は `.success` を返し、クロージャの戻り値がAssociated Valueに代入されます。
失敗した場合は `.failure` を返し、エラーがAssociated Valueに代入されます。
つまり、今回は `myFunc(isSuccess:)` が `String` を返すため、`Result<String, Error>` という型が生成されます。

### Result.mapError(_:)

次に2行目です。

```swift
    .mapError { $0 as! MyError }
```

こちらは失敗時のエラーの型をマップ（変換）するメソッドです。

`Result.init(catching:)` で生成した型は `Result<String, Error>` です。
`mapError(_:)` で `Error` 型を `MyError` 型に変換し、 `Result<String, MyError>` を生成します。

ここまでできたら、あとは通常の `Result` 型と同じように使えます。

```swift
switch result {
case .success(let string):
    // `string` を使う処理
case .failure(let error):
    switch error {
    case .foo(let message):
        // エラーハンドリング
    case .bar(let message):
        // エラーハンドリング
    }
}
```

## おまけ：なぜエラーの型を指定できないか考える

個人的な意見ですが、例えば `throws` の後ろにエラーの型を記述し、スローするエラーの型を指定できると使いやすいと思いました。

```swift
func myFunc(isSuccess: Bool) throws MyError -> String {
```

なぜこれが実装されないか考えたのですが、おそらく普通に実装しても様々なエラーが発生し得るからだと思いました。
自分では `MyError` 型しかスローしていないつもりでも、実装ミスで他のエラーがスローされる可能性は0ではありません。
実際、今回紹介した方法も `as! MyError` として強制キャストすることで実現しています。

## おわりに

これでエラーをスマートにハンドリングできるようになったと思います！
GitHubにサンプルのplaygroundを上げたので、よかったら実行してみてください。
https://github.com/uhooi/SwiftErrorResultSample/blob/master/ErrorResult.playground/Pages/Sample.xcplaygroundpage/Contents.swift

他にもいい方法がありましたら、コメントなどで教えていただけると嬉しいです。

## 参考リンク

- [init(catching:) - Result | Apple Developer Documentation](https://developer.apple.com/documentation/swift/result/3139399-init)
- [mapError(_:) - Result | Apple Developer Documentation](https://developer.apple.com/documentation/swift/result/3139402-maperror)