【Kotlin】kotlinでmichaelbull/kotlin-resultを使うススメ
関数型プログラミングによくあるResultって便利だよね
例えばRustでは標準で搭載されているFunctionalなエラーハンドリングの表現の一つで、
fn hoge() -> Result<String, Error> { match huga() { 0 -> Ok("OK".to_string()) _ -> Err(Error("error occured")) } }
みたいな感じにやると、受け取る側は例えば返り値をnullチェックなどをするまでもなく、 String
か Error
が帰ってきた方を仕分けすれば良くなる。
他にも flatmap
Either
とかイケてるメソッドと組み合わせればコードが綺麗にまとまるとかもあるが後で後述する。
Rustの仕組みとしては、
pub enum Result<T, E> { /// Contains the success value #[stable(feature = "rust1", since = "1.0.0")] Ok(#[stable(feature = "rust1", since = "1.0.0")] T), /// Contains the error value #[stable(feature = "rust1", since = "1.0.0")] Err(#[stable(feature = "rust1", since = "1.0.0")] E), }
一方Kotlinでは...
一応実装はあるんですが、
inline class Result<out T> : Serializable
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/index.html
A discriminated union that encapsulates successful outcome with a value of type T or a failure with an arbitrary Throwable exception.
うーん、KotlinのResultはどっちかというと、例外がスローされるかもしれない処理を runCatching
関数でラップして、明示的に処理を分けましょうみたいな感じで、いわゆる 戻り値Result
ではない感じです。
runCatching {
call.receive<AnswerRequest>() //ここが例外投げられるかもな処理
}
.onSuccess { call.respond(HttpStatusCode.OK) }
.onFailure { call.respond(HttpStatusCode.BadRequest, Error) }
誤解なきよう、これはこれで便利な訳です
それなら使おうライブラリ
今回はこちらです
このライブラリを使うことでkotlinコード全体を一気にモナモナさせていくことが可能です。。。
実装は以下
/** * [Result] is a type that represents either success ([Ok]) or failure ([Err]). * * - Elm: [Result](http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Result) * - Haskell: [Data.Either](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html) * - Rust: [Result](https://doc.rust-lang.org/std/result/enum.Result.html) */ sealed class Result<out V, out E> { companion object { /** * Invokes a [function] and wraps it in a [Result], returning an [Err] * if an [Exception] was thrown, otherwise [Ok]. */ inline fun <V> of(function: () -> V): Result<V, Exception> { return try { Ok(function.invoke()) } catch (ex: Exception) { Err(ex) } } } } /** * Represents a successful [Result], containing a [value]. */ data class Ok<out V>(val value: V) : Result<V, Nothing>() /** * Represents a failed [Result], containing an [error]. */ data class Err<out E>(val error: E) : Result<Nothing, E>()
面白いところですが、 kotlinの enum class
を使っていません。kotlinのenum classはジェネリクスみたいにタイプパラメータを持てなかったり、他のクラスみたいに
パラメータでコンストラクタする訳ではないとか理由はあるでしょう。
enum class Result<V, E>(ok: V, err: E) {} // こんなのとかできない enum class Color(val rgb: Int) { RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF) } // あくまで最初から中身は決まっていて、rgbパラメータで初期化するとかの意味ではない。
このmichaelbull/kotlin-resultのライブラリは、単純にenumではなくclassとdata classの組み合わせですが、とても上手く動きます。
使いっぷり
fun returnResult(): Result<String, Error> = TODO() fun test() = returnResult() .mapBoth( success = { str -> println(str) }, failure = { err -> println(err) } )
michaelbull/kotlin-resultのResult型には様々なメソッドがついており、例えば mapBoth
を使えばResultを受け取って、
Ok()
Err()
data classに入った値を安全に処理を分岐させることができます。 mapBoth
はそのなの通り、Ok()をもらっても、Err()をもらっても同じ戻り値を返す時に便利で、例えば今回の場合は Unit
を返してるので使っているだけです。例えばここから、エラーの方を別のエラーで返したい場合は mapError
mapEither
などもあります。これらはRustやScalaなど使っている人はおなじみだと思います。それがKotlinでも使えるとなるとテンション上がりますね😄
もちろん関数型の人たち御用達な flatmap()
もあります(Rust使い用に andThen
もありますが、絶対この作者Rust好きだから入れただろ...)
fun returnResult1(): Result<ArgA, Error> = TODO() fun returnResult2(a: ArgA): Result<ArgB, Error> = TODO() fun returnResult3(b: ArgB): Result<ArgC, Error> = TODO() fun test(): Result<ArgC, Error> = returnResult1() .flatMap { a -> // a type is ArgA returnResult2(a) } .flatMap { b ->// b type is ArgB returnResult3(b) }
こんな感じで、共通なErrorの型を持ってResultで返してくれる関数をひとまとめにした時に、flatMapでエラーが発生した時に止めてくれて、 Ok()が帰ってくる限り処理を続けるみたいなことができます。
kotlinに嬉しいnullableからの変換
michaelbull/kotlin-resultには toResultOr
というメソッドがあり、kotlinの代表的な型表現である ?
をResultに変換できます。
fun returnResult1(): ArgA? = TODO() fun returnResult2(a: ArgA): Result<ArgB, Error> = TODO() fun test(): Result<ArgB, Error> = returnResult1() .toResultOr{ Error("ERROR") } .flatMap { a -> // a type is ArgA returnResult2(a) }
このように、例えばnullableな返り値の関数も、nullの場合をエラーとみなしてメソッドチェーンを繋げることが可能です。 そんな感じで、とてもいいぞmichaelbull/kotlin-resultという宣伝記事でした。