今回は The Basics
における Error Handling
をひととおり眺めていく中で、もう少し見ておきたい気がする細かな部分を眺めてみます。具体的には、これまでの話の中で出てきた Result
や LocalizedError
のようなエラー処理にまつわる各種型について意識を向けてみようと思ってます。どうぞよろしくお願いしますね。
今回は参加者の一般参加者を募っての開催、ゆめみの外の人たちも訪れての開催になる見通しです。
————————————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #185
00:00 開始 00:43 これまでのあらすじ 04:33 Result 型を用いたエラー表現 07:14 Result でエラーを扱わない場合 08:52 Result 型の定義を見てみる 09:12 Result の Failure は Error を要求 10:25 エラーを正常系にリターンして委ねる 11:50 エラーをチェーンしていく 12:32 Result とエラーハンドリングとの相性 13:32 成功と失敗のどちらかで、それに対応する値を持つ 14:04 成功をチェーンしていくメソッド 15:28 失敗をチェーンしていくメソッド 16:30 新たな成功と失敗の状況想定でチェーンしていく 17:56 Result 型からエラーを送出 19:52 値とエラーのマップ振替 23:29 Result 型の既定の実装 24:29 Result 型の等価比較演算にみる不思議な実装 26:28 Result 型におけるハッシュ値 27:21 Result 型が init(catching:) を Error 限定にしている理由 30:03 クロージングと次回の展望 —————————————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #185
さて、始めていきましょう。まずエラーハンドリングの話はほぼ終わったんですが、その中で出てきた細かい部分について、今回は少し触れてみたいと思います。エラーハンドリングを学ぶ機会ってあまり多くないかなと思うので、細かい部分を後回しにせず、しっかり見ていこうと思います。
今回初めて参加した方もいるかもしれないので、少しおさらいをすると、エラーハンドリングについて「try
」「catch
」を使って行うというのがSwiftの文化ですね。「The Swift Programming Language」の中では、このエラーハンドリングの例が載っていました。
エラーハンドリングの醍醐味は、エラーからの復帰ができることですが、例だと復帰までの流れがあまり分かりにくい感じです。それで、どういうふうにエラーハンドリングをしっかり行うかをおさらいしたいと思います。
その中で特に目立ったのが2つあります。まず「try?
」や「try!
」などで話に出てきた「Result
」型を使ってエラーハンドリングを行う方法です。これはオプショナルを返す方法と比較して、どちらが良いのかという話もありました。「Result
」型は制御フローを明確にできるので、表現力が適切です。さらに普通のエラーハンドリングのパターンに変換することが可能です。
もう一つは、エラーを独自に作るために基本的にはスローするエラーが必要ですが、手軽にエラーをスローしたい場面もあります。その場合、「String
」型を「LocalizedError
」に準拠させる技を利用することができます。「String
」をそのままエラーとして投げることができます。これは、いくつかの言語で「String
」をエラーとしてスローできる機能と似ています。
「LocalizedError
」というプロトコルでエラーを簡単に表現できるので便利です。私自身はあまり知らなかったので、この辺りを細かく見ていきたいと思います。また、余談としてもう一つ話したいことがあるのですが、それは時間があれば触れてみます。
まず「Result
」について見ていきたいと思います。「Result
」型は、成功と失敗を表現する方法で、成功時には値を返し、失敗時にはエラーを返すことができます。古くはオプショナル型で成功失敗を表現する方法もありましたが、これは現在ではあまり主流ではありません。
「throws
」を使う場合は、細かいエラーを定義しスローする必要がありますが、そこまでしなくても良い場面も多いです。そこで「Result
」型を使うと、独自のエラーを定義せずともエラーを処理することが可能です。
ただし、「Result
」型を使うときにはエラープロトコルが必要になります。例えば、「Void
」を使ってみてもエラープロトコルに準拠していないためうまくいきません。以下に少しコードを書いてみます。
enum MyError: Error {
case runtimeError(String)
}
func myFunction() -> Result<Void, MyError> {
// 何らかの処理
return .failure(.runtimeError("エラーが発生しました"))
}
このように、やはりエラープロトコルに準拠したエラーを返さなければならないので、使い勝手は「throws
」と似ている部分があります。ですが、場合によっては「Result
」型の方が直感的で分かりやすいでしょう。
まとめると、エラーハンドリングの方法は複数ありますが、「Result
」型や「LocalizedError
」を使うことで、より柔軟で表現力豊かなエラーハンドリングが可能になるということです。どうぞそれぞれ試してみて、最適な方法を見つけてください。 そうですね、他に何かありますかね。マップエラーくらいしか思いつかないですね。
まあ、今はここまでにしておいて、次のテーマについてゆっくり考えましょう。この話はとりあえず終わりにして、今日はリザルトについて見ていきたいと思います。まず、リザルト型の定義を辿ってみます。
リザルト型はフィーラーがエラープロトコルに準拠している必要があります。これによって、他の言語でよく見られるリザルト型とも共通点が出てきます。他の言語ではリザルトをレフトとライトで表現しますが、人によっては右側が正常系という共通認識があります。ただ、リザルト型のフィーラーは名前から明確に失敗を示します。さらに、エラープロトコルによってエラー系としての制約がかけられているのが特徴です。
これにより、エラーハンドリングはスローズと同じように使えるというのが大きなメリットです。正常系にリターンして処理を委ねることができるという点も興味深いです。この辺りはどう使い分けるかが鍵になりますね。エラーハンドリングにおいて正常系と異常系のリターンタイミングを分けることができるのは、関数の設計において重要です。API設計者側はリザルトを返すことで制御側に投げ、受け取った側がうまく処理するという流れになります。
前回の話では、ストリームを使うことが一般的になってきており、そのときにリザルトが相性が良いという話がありました。私自身はストリーム系にあまり馴染みがないので実感はないですが、リザルトをチェーンすることで処理がしやすいというのは理解できます。エラーハンドリング自体は通常チェーンする考え方ではないですが、似たようなことにはなりますね。ただ、リザルトはチェーンメソッドを持っているので、使いやすさが違う部分があると思います。
コメントでいただいた「ブーキャッチ」と同じ感覚をリザルトに持たせるというのは、ゲットスローズを使うことで実現できます。リザルト型はいろいろな状況に柔軟に対応できる点が面白いです。ワンクッション置く感じでフロキシーのようなクッションになることもあります。
ただ、トライキャッチでスマートに処理したい場合には、ゲットメソッドを呼ばないといけない場合もあります。これによって、余計な手順が増えることがありますが、それは仕方ないですね。
リザルト型は成功か失敗のどちらかを表現する値です。成功を示す「success」と失敗を示す「failure」の2つの関連値を持つ列挙型があります。成功側は指定した型であれば何でも入りますが、失敗側はエラーに準拠する必要があります。
また、リザルト型には「get」や「map」などのメソッドがありますが、ストリームをあまり使わない私には馴染みがないですね。それでもリザルトに対してマップを使うことの利便性は理解できます。 チェインしていくときに大事になるものとして「マップ」を使うと、トランスフォームを利用しますね。正常な場合、つまり成功した場合の結果をさらに map
で別の位置に変換します。このあたりは配列の map
と同じ雰囲気、いやオプショナルの map
と似たような雰囲気かもしれません。エラーだった場合にはエラーをそのまま受け渡すという感じです。
次へチェインしていくというのは面白いですね。どんどん変化させつつ、エラーがあれば対応していき、最後にエラー処理をするという手法です。これはエラーハンドリングの基本と言えるかもしれません。コンカレンシーにおけるエラーハンドリングの原点といった感じですね。こういうのは面白いですし、私の好みです。
もう一つが mapError
ですね。これはエラーだったときに新たなエラーにマッピングするものです。正常な場合はそのまま通過させます。エラーハンドリングでいろんなエラーが起こりますが、このAPIが返すエラーはこの系統のエラーに統一する、といったことができます。エラーをキャッチする際に困ることが多いので、整えていくのが大事な方法です。リザルト処理をすると、それが簡単に使えるというわけです。
さらにもう一つ、興味深いのが flatMap
です。フラットマップはトランスフォームがリザルトを返すパターンで、それを普通にリザルトフェイラーに上乗せする感じです。最初は存在意義がわかりませんでしたが、使うことはあるでしょう。特にリザルトをよく使う人にはよく知られているでしょうし、正常の場合はそのまま返し、エラーがあればそれをスローするという嬉しい機能ですね。
例えば、わざわざ guard case let value
を使わずに済むのは便利ですし、スイッチケースも不要になることが多いです。結局、アクセスのバリューだった場合に色々と面倒なことが省けるのがいいところですね。
これを使えば Result
の扱いが簡単になりますし、シンプルなのに機能豊富で面白いですね。flatMap
のエラー版もあり、成功時にはそのまま通過し、エラー時にさらにマッピングするので、より柔軟に対応できます。
こういった操作を試しながら、自分のプロジェクトに活かしていくと良いでしょう。 例えばある関数があり、これはリザルト型で Int
とエラーを返すとします。そして、この関数が return 7
を返すようにします。リザルトを使って Int
の値とエラーを処理することにします。
リザルトに対して flatMap
を使用し、関数Aを渡します。しかし、エラーが揃っていないとエラーが生じます。これはPlayground上でのエラーかもしれません。関数がリザルトを返す場合、その関数は Int
を取って Int
を渡すことになります。この部分を見逃していたので意味がわからなかったのです。
普通のオプショナルのマップと同じように、ある値を受け取り、それを元に何かを行うといった感じです。例えば、flatMapを使用すると、そのエラーの場合には関数が呼ばれずにエラーになるという流れです。これはエラー処理の場合のフラットマップの逆版とでもいえます。
リザルトのメソッド全てを理解するため、規定の実装についても気になります。例えば、リザルトがフェイラー(エラー)だった場合、そのエラーそのものが取り込まれる特別な初期化メソッドがあります。
リザルト型が Equatable
である場合、エラーが Equatable
であれば、リザルトに対して ==
や !=
を使用できます。このような場合に規定の実装が必要で、必要条件を満たすことで Equatable
な挙動が得られるようになります。これは条件付き準拠(Conditional Conformance)という仕組みを活用しています。
以上の内容から、リザルト型の処理やエラーハンドリングについて理解が深まりました。 確か、この部分について考えると良いのかなと思います。これ、一緒に出してしまっても良いのにと感じますが、どうでしょうか。もし分かる人がいたら、コメントで教えてくださいね。
あと、「Hashable(ハッシュアブル)」についても話したいと思います。どちらも「Equatable(イコータブル)」か「Hashable」どちらか一方で十分なこともあります。関連のあるデータを持っているとダメな場合があるなど、細かいルールがあるんですが、その辺りは試してみないと分かりません。
例えば、「Success」と「Failure」が「Hashable」だった場合、それぞれの型も「Hashable」になります。また、どういう実装になっているかは分かりませんが、「Hashable」として振る舞えるように作られています。これは非常にシンプルな定義ですね。
次に気になった点ですが、「Equatable」の記述が少し不思議な感じがしました。また、エラーの時のキャスティングについても話しました。この型がエラーでなければ意味を成しません。このときに初期化子としてキャスティングがありますね。しかし、エラーハンドリングはどんなエラーが返るか分からないので、適切に処理する必要があります。
エラーハンドリングの関数を結果タイプに変換するというキャスティングの話ですが、この部分は省略可能かどうかといった議論がありましたが、最終的にエラーが結果型に変換されるというのは分かりやすいですね。
こうやって関数をエラー処理の機構から結果処理の機構に変換するというのは非常に興味深いです。このように、エラーハンドリングと結果処理の機構が相互に入れ替え可能になってるというのが面白いポイントです。特に、結果型がエラーハンドリングに同じように使えるというのは秀逸ですね。
さて、リザルト型の話をしていたら時間になってしまいました。また次回、ローカライズドエラーなどについて話せると良いですね。次回に持ち越しましょう。
リザルト型のエラーハンドリングの話ですが、これも非常に面白い話題です。もし何か質問があればどうぞ。リザルト型について詳しく知っている人には分かりやすかったかもしれませんが、初めての方には目新しい機能だったと思います。ぜひ、リザルト型をいじって遊んでみると面白いのではないかと思います。
時間もちょうど良いので、今日はこれぐらいにしましょう。お疲れさまでした。ありがとうございました。