Arrowを使用したKotlinの機能エラー処理

画像



こんにちは、Habr!



誰もがランタイム例外を愛しています。 コードを書くときに何かが考慮されていなかったことを見つけるより良い方法はありません。 特に、例外が何百万人ものユーザーの間でアプリケーションをドロップし、このニュースが分析ポータルからのパニックメールで届く場合。 土曜日の朝。 田舎旅行にいるとき。



この後、エラー処理について真剣に考えます-Kotlinが提供する可能性は何ですか?



最初に頭に浮かぶのは、try-catchです。 私にとって-素晴らしいオプションですが、2つの問題があります:



  1. 結局のところ、これは余分なコードです(コードの周りの強制ラッパーは、最良の方法で可読性に影響しません)。
  2. (特にサードパーティのライブラリを使用する場合)catchブロックから常にエラーが発生するわけではないため、エラーの正確な原因に関する情報メッセージを取得することはできません。


上記の問題を解決しようとするときに、try-catchがコードを変換するものを見てみましょう。



たとえば、最も単純なネットワーククエリ実行関数



fun makeRequest(request: RequestBody): List<ResponseData>? { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } }
      
      





のようになります



 fun makeRequest(request: RequestBody): List<ResponseData>? { try { val response = httpClient.newCall(request).execute() return if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { null } } catch (e: Exception) { log.error("SON YOU DISSAPOINT: ", e.message) return null } }
      
      





「それほど悪くはない」と誰かが言うかもしれません、「あなたとあなたのコットリンはすべてのコードシュガーを欲しがっています」と彼は付け加えます(これは引用です)-そして彼は...二度正しいでしょう。 いいえ、今日はホリバーはありません-誰もが自分で決めます。 私は、各フィールドの解析がtry-catchでラップされ、各catchブロックが空である自己記述jsonパーサーのコードを個人的に支配しました。 誰かがこの状況に満足している場合-手にフラグ。 もっと良い方法を提供したいです。



ほとんどの型付き関数型プログラミング言語には、エラーと例外を処理するための2つのクラスがあります: TryBoth 。 例外を処理し、ビジネスロジックエラーを処理します。



Arrowライブラリーを使用すると、これらの抽象化をKotlinで使用できます。 したがって、上記のリクエストを次のように書き換えることができます。



 fun makeRequest(request: RequestBody): Try<List<ResponseData>> = Try { val response = httpClient.newCall(request).execute() if (response.isSuccessful) { val body = response.body()?.string() val json = ObjectMapper().readValue(body, MyCustomResponse::class.java) json?.data } else { emptyList() } }
      
      





このアプローチはtry-catchの使用とどのように違いますか?



まず、このコードを読んだ人は誰でも(おそらくそうなるでしょう)、コードの実行によってエラーが発生する可能性があることを署名によって既に理解し、それを処理するためのコードを記述できます。 さらに、これが行われない場合、コンパイラは誓います。



次に、エラーの処理方法に柔軟性があります。



Tryの内部では、実行のエラーまたは成功は、それぞれ失敗クラスと成功クラスとして表されます。 エラー時に常に何かを返すようにするには、デフォルト値を設定できます。



 makeRequest(request).getOrElse { emptyList() }
      
      





より複雑なエラー処理が必要な場合は、foldが役立ちます。



 makeRequest(request).fold( {ex -> //  -       emptyList() }, { data -> /*    */ } )
      
      





recover関数を使用できます。TryがSuccessを返した場合、その内容は完全に無視されます。



 makeRequest(request).recover { emptyList() }
      
      





Tryで.monad()ファクトリを呼び出すことにより、一連のコマンドを使用して成功の結果を処理する必要がある場合は、内包表記(ScalaのArrow作成者が借用)に使用できます。



 Try.monad().binding { val r = httpclient.makeRequest(request) val data = r.recoverWith { Try.pure(emptyList()) }.bind() val result: MutableList<Data> = data.toMutableList() result.add(Data()) yields(result) }
      
      





上記のバリアントはバインディングを使用せずに記述できますが、その後は別の方法で読み取られます。



 httpcilent.makeRequest(request) .recoverWith { Try.pure(emptyList()) } .flatMap { data -> val result: MutableList<Data> = data.toMutableList() result.add(Data()) Try.pure(result) }
      
      





最終的に、関数の結果は次の場合に処理できます。



 when(response) { is Try.Success -> response.data.toString() is Try.Failure -> response.exception.message }
      
      





したがって、Arrowを使用すると、理想的なtry-catch構造とはかけ離れたものを、柔軟で非常に便利なものに置き換えることができます。 Arrowを使用するもう1つの利点は、ライブラリがそれ自体を機能的であると位置付けているにもかかわらず、古き良きOOPコードを書き続けながら、そこから個々の抽象化(たとえば同じTry)を使用できることです。 しかし、私はあなたに警告します-あなたはそれを好きで参加するかもしれません、数週間であなたはHaskellを勉強し始め、あなたの同僚はすぐにコードの構造についてのあなたの推論を理解しなくなります。



PS:それは価値がある:)



All Articles