KotlinでResult型使うならkotlin-resultを使おう
この記事はand factory Advent Calendar 2020 の2日目の記事です。昨日は@kusumi_katsumiさんの【Android】Single ActivityにおけるsharedViewModelのスコープについて【Koin】でした😍
Qiitaと悩みましたが、僕は普段noteに書いているのでこちらに書くことにしました(Qiitaのアドベントカレンダーに、Qiitaじゃなくてもいいよって書いてあったので)
What is Result型
KotlinにはResultが標準でありますが、例外を処理するための機構で、他言語で一般的に使われるいわゆるところのResult型とは違います。
Result型はResult<T, E>でよく表現される型で、例外の機能を使わずにエラーを処理する書き方をすることができます。
同じJVM言語のScalaでは例外とは別に提供されていたり(Scalaの場合ResultではなくEither)、関数型言語のHaskell、Elm、そしてRustも言語機能として入っています。Kotlinと近しいところで、SwiftでもResultが入っているようです。
例えばRustでは、このように書くことができます。
fn get_user(): Result<User, Error> {
// ユーザーをエラーなく取得できたら
ok(u)
// もし何らかのエラーが有った場合とか
// Err(Error())
}
Result型を使うことで、例外とは異なり、呼び出し側でのハンドリング漏れをなくしたり、想定していないエラーの発生を予防させることができます。
特にAndroid開発では、例外処理のハンドリングをうっかり忘れてしまうとアプリがクラッシュしてしまうため、それを予防する意味でも非常に有用です。
独自のResult型を入れた場合
Kotlinには例外処理用のResultが入って入るものの、上記で紹介したようなResult型は残念ながら入っていません。
ですが、Result型自体はOkとErrを持ったシンプルなデータオブジェクトなので簡単に定義することができます。例えばGoogleのサンプルコードなんかでも下記のように定義して使っています。
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
}
実際、僕が関わっているプロダクトでもこのようなシンプルなResult型を使っていました。
ですが、このようにシンプルに定義した独自Result型を使っていると、結構問題があります。
例えば、①ユーザーデータを取得して、②ログイン処理して、③その他色々セットアップする(例えばFirebase SDKとか)というユースケースのコードを、このResult型で書いてみるとこうなります。
だいぶ辛いネストが発生しています😱。毎回whenで結果を取り出し、都度都度Errorのハンドリングをしています。
流石に1関数で書くと見苦しいので関数分割をしたりすると、一見見通しやすくなりますが、ほぼコールバック関数のようになってしまいます。
❌ ネストが深くなる
❌ コールバック関数のようになり処理が複雑になる
❌ OkのときErrのときの重複コードが発生する
また上記のResult型はエラーの方の型定義をしておらず、一律Exception型だったので、呼び出し元がなんのエラーが帰ってくるかわからず、結果としてハンドリング漏れが発生することがありました。
標準で組み込まれているRust、ScalaのResult構文を見る
では、標準でResult型が入っているRustやScalaの場合はどうなるでしょう。実は、Resultを標準で提供している言語の場合、非常に多くのメソッドがResultに生えていて、もっと快適に、シンプルに書くことができてしまいます。
例えばRustで同じユースケースのコードを書くとこうなります。
fn main() {
let r = get_user()
.and_then(|u| login(&u))
.and_then(|_| setup_something());
match r {
Ok(_) => println!("Success login"),
Err(err) => println!("Login error: {}", err),
}
}
Kotlinで書いたあの地獄のネストが嘘のようにシンプルにかけています💡
and_then()関数はOkのときに値を取り出して新しいResultを返す関数です。このようにand_thenを使えばOkのときだけの継続処理を書いていくことができます。
and_thenだけでなく、RustのResultには多くの便利メソッドが定義されて、ネストせずに心地よく書いていくことができます。
また、同じJVM言語のScalaのEitherの場合はこう書けます。
def main(args: Array[String]): Unit = {
val result = for {
user <- getUser()
_ <- login(user)
_ <- setupSomething()
} yield ()
result match {
case Right(_) => println("Success")
case Left(e) => println(e)
}
}
Scalaの場合もRustと同様に、成功したときの継続処理をfor式でまとめて書けます(もっといい感じに書けるはずだけどこんなんなっちゃった)。
kotlin-result
Resultを返す複数の関数を呼ぶ場合は、Scala、RustのようなResultの機能がないと、書いていて結構厳しい場面が多いです。そこでKotlinにもちゃんとしたResultの言語機能を提供してくれるライブラリがありました。それがkotlin-resultです。
GithubのReadmeにもあるように、KotlinにHaskell、Elm、Scala、そしてRustと同様の機能を持ったResult型を提供するライブラリです。
このkotlin-resultを使った場合、先程のネストが深かったコードがこんなかんじになっちゃいます。
あのネストが嘘のようにきれいになりました😍
flatMapという関数が、Okのときに値を取り出して新しいResultを返す関数です。
getUser()して、Okだったらlogin()、OkだったらsetupSomething()してます。
mapBothではsuccessのときとfailureのときの処理を書くことができます。flatMapでチェーンしている途中でErrを返却している場合はfailureの部分で処理できます。
また、kotlin-resultではResult<V, E>でErrorのときの型も指定するので呼び出し側は何が返ってくるのかがひと目でわかります。
独自のエラーオブジェクトを返してもいいですし、もちろんExceptionをErrorに指定して返すことも可能です。
kotlin-resultでよく使う関数
kotlin-resultは非常に便利で、Result型を使う場合には僕はこれを使うようにしています。
もともと別の言語でResult型を使ったことがある人はこのkolint-resultのwikiを見ればすぐに使い始めることができると思います。基本的には、他言語で一般的に使われる関数と同様のものが提供されています。
その中でも僕が基本的にこれを使っているというものを紹介します。
flatMap(andThen)
ResultがOkのときに処理を実行して新しいResultを返します。flatMapのほうが一般的のような気がしますが、Rustではand_thenで、作者はRustが好きなようなのでwikiでもandThenが載ってます。
flatMapとandThenどちらでも同じなので、使うときは混ざらないようにどちらかプロジェクトで決めて使いましょう。
getUser().flatMap {
// Okのときに入る
login(it)
}
flatMapは新しいResultを返すのでErrを返すことも可能です。例えば、Okなんだけど、特定の値はエラーにするとかもできます。
val result: Result<User, Error> = getUser().flatMap {
if (it.name.isEmpty()) {
Err(UserNameIsEmptyError)
} else {
it
}
}
map
flatMapと似てますが、mapもよく使います。Okのときの値を取り出して加工したOkを返します。
val result: <String, Error> = getUser().map {
it.name + "さん"
}
mapBoth(mapEither)
mapBothはsuccessとfailureにそれぞれOkとErrが入ってくる関数です。mapEitherも同様です。whenを使ってもいいですが、そのままメソッドチェーンできるのと、それぞれのときの値をitで取得できるので便利です。
getUser().mapBoth(
{ login(it) },
{ handleError(it) }
)
個人的には名前付き引数を使ってあげたほうが可読性があがるのでつけるようにしています。
getUser().mapBoth(
success = { login(it) },
failure = { handleError(it) }
)
get / getOr
get()はOkの値を取り出します。Errの場合はnullが返却されます。見つからなかったよエラー的なものをnullにして扱いたい場合などに便利です。
val user = getUser().get()
getOr()もOkの値を取り出しますが、Errのときの値を指定して取得できます。
val user = getUser().getOr(User("", ""))
when
今までは便利なメソッドチェーンたちでしたが、時には普通にwhenで書いたほうがいいこともあります。
when (val r = getUser()) {
is Ok -> r.value // Okの値はvalueで取得できる
is Err -> r.error // Errの値はerrorで取得できる
}
例えばErrの場合はさっさと処理してOkのときの値を取り出したい場合などはwhen式が便利です。
fun executeLogin() {
val user: User = when (val r = getUser()) {
is Ok -> r.value
is Err -> {
handleError(r.error)
return
}
}
login(user)
}
早期リターン(early return)する場合もこのwhen式を使ったやり方が一番良さそうな気がします。
val user: User = when (val r = getUser()) {
is Ok -> r.value
is Err -> return
}
whenを使って書く場合はフラットになるときが良いかなと思っています。ネストする場合はflatMapやmapBothなどを使ってチェーンして書くほうがいいでしょう。
終わりに
KotlinでResult型を使うときに是非取り入れたいライブラリkotlin-resultの紹介でした!
ここではKotlinにResult型を導入する前提で書いていますが、Kotlinは標準でResult型を導入していない言語です。kotlin標準のResult(run cacthingのほう)も、例外を適切に処理できるようにする機能ですし、例外を使って書いていくという方が良いのかも知れません🤔
それでもResult型を使ってコードを書く場合は、他言語のように便利なメソッドがある程度必要かなと思っています。その場合はkotlin-resultのようなライブラリを入れることで快適に書けるようになります🙋♂️
この記事が気に入ったらサポートをしてみませんか?