ひと通り見終えた感じの個人的にお気に入りな技術ブログ「その Swift コード、こう書き換えてみないか」ですけれど、余興的に続く事項の中にもおもしろそうな話題がありそうですので、もう少し続けて気になるところを眺めていきますね。そんな今回は前回の続きで Result
と throws
の相互変換的な話題のところから見ていくことにします。よろしくお願いしますね。
———————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #279
00:00 開始 00:35 Result 型とエラーハンドリングの相互変換 01:24 Result 型とは 02:28 Result 型の get メソッド 03:59 Failure が Error 準拠なのが良い 05:40 条件付き型拡張で表現するなら 06:40 Result 型と、いわゆる Either 型の表現比較 08:19 エラーハンドリングを Result 型に変換 11:57 相互変換まわりのおさらい 12:32 Result.init の Concurrency 対応 14:02 結果をマップする 15:24 Combine 用の機能もあるらしい 18:00 デリゲートやコールバックを Concurrency で扱う話 18:52 コールバックを Concurrency に変換 23:13 Continuation は Sendable 25:10 クロージングと次回以降の展望 ————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #279
では始めていきますね。今日も引き続き、田中陽川さんのブログ記事「このSwiftコードを書き換えてみないか」という提案プロジェクトを見ていきたいと思います。これも一通り見た感じにはなりますが、その中の評価的な部分についてお話しします。
前回は「Result」と「Throwing Functions」、つまりエラーを返す可能性のある関数と Result
型との相互変換について触れました。結局のところ、 Result
型に搭載されている Throwing Function についての話になりましたが、相互変換についてはあまり詳しく触れませんでした。
まず、 Result
型ですが、成功した場合と失敗した場合を関連値として持てるような設計になっています。 Either
型として知られる設計ですが、失敗したときには関連する値が必ずエラープロトコルに準拠しているという特徴があります。このおかげでエラーハンドリングが非常に効率的になっているわけです。
Result
型をエラーハンドリングに変換するメソッドとしてよく挙げられるのが get
メソッドです。この get
メソッドを呼ぶと、成功した場合は成功した値を返し、失敗した場合は throws
でエラーがスローされます。これは非常に美しい関数ですね。名前が少し直感的ではないかもしれませんが、考えてみれば妥当だと感じられるでしょう。
この get
メソッドが動作するためには、Result
のエラー部分がエラープロトコルに準拠している必要があります。そのため、エラーが発生したときにはそれをそのままスローできる、という設計になっています。
例えば Result
型の値があったとき、 if case .failure(let error)
という形で失敗を確認することができます。そして、この失敗の場合にはそのままエラーをスローすることができます。これが Result
型の応用例のひとつです。エラープロトコルに準拠しているおかげで、非常にスムーズにエラーハンドリングが行えます。
一般的な Either
型の場合、left
と right
で成功と失敗を表現しますが、この場合にはスローすることが難しいです。例えば、let either: Either<Error, Int> = .left(someError)
といった形になります。この場合、エラーをスローするためにはいくつかの追加処理が必要になります。
Either
型でもエラーがスローできるようにするには、エラー部分がエラープロトコルに準拠している必要があります。例えば、enum Either<L, R> { case left(L), right(R) }
の形で Either
型を定義し、エラー部分がエラープロトコルに準拠している場合には get
メソッドを用意してスローできるようにするといった方法があります。
しかし、Either
型が持つ左右対等の関係を保つのは難しくなります。どちらが成功でどちらが失敗なのかの判断が難しいため、結局は Result
型のように成功とエラーを明確に分けたほうが使いやすいという結論になります。
このように、成功と失敗を明確に分けた Result
型は、エラーハンドリングにおいて非常に便利で効率的な方法となります。今回の議論を通じて、その利点と使い方を再確認できたかと思います。 まず、「賢いは言い過ぎかな」というのは文脈が不明確なので無視します。このあとは、SwiftのResult
型とエラーハンドリングについて解説していますね。
Result
型にはイニシャライザが搭載されていて、エラープロトコルに準拠していれば、エラーのハンドリングとして変換できます。逆に、リザルト型にはイニシャライザがあり、例えばイニットキャッチなどの面白いイニシャライザが搭載されています。このイニシャライザに対して、スローイング関数(throwing function)を渡して成功を返すことができます。
具体的には、スローイング関数の中でエラーが発生するとそのエラーがResult
型の失敗として変換され、エラーが発生しない場合は成功として変換されます。以下はそのサンプルコードです。
let result = Result {
try someThrowingFunction()
}
ここで、someThrowingFunctionがエラーをスローすると失敗として、エラーが起きなければ成功としてResult
型のインスタンスが得られます。
また、ファイルマネージャーなどのスローイングするメソッドを使って、成功した場合に何かをする処理をResult
型で表現することができます。以下はその例です。
let fileResult = Result {
try FileManager.default.copyItem(atPath: sourcePath, toPath: destinationPath)
}
このように、スローイング関数をResult
型に変換したり、Result
型からスローイング関数へ変換したりできます。
さらに、Result
型は列挙型でスマートに書けますが、キャッチする際にエラー処理を加えることもできます。例えば、以下のような形です。
let result = Result { /* some throwing function */ }
switch result {
case .success(let value):
print("Success with value: \\(value)")
case .failure(let error):
print("Failed with error: \\(error)")
}
これにより、スローイング関数のエラーを捕捉して適切に処理できます。
最後に、Result
型のmap
およびmapError
メソッドについて説明します。map
メソッドは成功値を別の型に変換し、mapError
メソッドは失敗値(エラー)を別の型に変換します。以下の例では、エラーメッセージをカスタムエラー型に変換しています。
let stringResult: Result<String, Error> = .failure(SomeError.someFailure)
let newResult = stringResult.mapError { error -> CustomError in
return CustomError(customMessage: error.localizedDescription)
}
このように、Result
型を使って柔軟にエラーハンドリングや値の変換を行うことができます。 別なリザルトに変換するとき、ネクロジャーとエラー、それからプラットフォームの違いに注意が必要です。確かにそのような状況はあり得ると思います。Combine
とPublisher
についてですが、「Combine」を使っていないとよく分からないかもしれません。しかし、Publisher
を使ったリザルト変換はあります。リザルトにPublisher
の型も含まれるのか調べてみます。
成功時と失敗時に動くということなら、確認が必要です。成功の場合、Combine
を入れないといけないのかもしれません。もしPublisher
があるのであれば影響を辿る必要があります。Combine
の中でエクステンションしているのか、エクステンションをリザルトにしていて、その結果Publisher
としてリザルトの成功と失敗の型を選べるようになっているわけです。
具体例として、Publisher
型にはaccount.put.success
などがあり、リザルトとして使えます。Combineはリザルトとスローの総合変換を想定しているのかもしれません。このブログのタイトルがリザルトの紹介になっているので、このリンク元がリザルトとスローの総合変換について説明しているようです。
次に移りましょう。デリゲートパターンやクロージャーパターンについてです。クロージャーパターンという言葉は初めて聞くかもしれませんが、一般にコンプリッシャーバンドラーやリザルトバンドラーとして知られているものです。これをasync
の関数にブリッジするのが一般的な方法です。
async
関数でリジュエルコンティネーションを使うことで、特定のタイミングで値を返す処理ができます。これはコンプリッシャーの有名な機能です。さっきの評価と概ね同じ内容です。
まずasync
の関数を用意し、その中で処理します。コンプリッシャーバンドラーが結果を返すことになります。このとき、元々のasync
関数内でawait
でwithCheckedContinuation
を使用します。今回はエラーハンドリングも行いたいので、withCheckedThrowingContinuation
を使い、try await
で非同期例外処理を行います。このwithCheckedThrowingContinuation
は、終了を知らせてawait
状態から再開するためのものです。
具体的には、withCheckedThrowingContinuation
の中でタスクを処理します。バックグラウンドで処理する必要がある場合もあります。このクロージャー処理は一度終わらせ、その後でタスクを再開するようにします。例えば、データタスクをコンプリートする際の処理を行います。
以上が基本的な流れです。 なので、ここでエラーがあった場合にはリジウムのスローイングでエラーとなります。そしてエラーでなかった場合にはリジウムリターニングデータですね。
次に、そのタスクを実行させていただけますか?という質問がありました。タスクを実行することによって、タスクが終了したときに結果が返ってきます。この結果が返ってくる際に、このイベントがアジデオタイム(おそらく「アイドルタイム」)が廃止されるように動作します。これは、一度このクロージャーが終了すれば良いわけです。終了後に、コンティニュエーションが広がっていけば良く、このコンティニュエーションがスレッドタスクを待っているのが面白い点です。
具体的には、ミスチェックコンティニュエーション
(正しくはTaskContinuation
)を見ると、これを自由にタスク内で扱うことができます。バックグラウンドの異なるタスクに渡すことが可能で、渡した先でデータを読むなどの処理ができます。また、このコンティニュエーションはデリゲートなどとも組み合わせが可能で、応用次第で広く使える点が特徴です。
時間も良い具合なので、この辺りで終わりにしますかね。次回はデリゲートパターンを非同期関数にする回を見て、さらにNotificationCenter
をSwiftコンカレンシーで使うことを検討します。この部分はコンティニュエーションが中心となり、自由に取り回しが利くため、具体的にどう使えるのかを見ていきます。それもコンプリションハンドラーの話と絡めて検討する予定です。
それでは、次回の動画でお会いしましょう。ありがとうございました。