Kotlinでのエラーハンドリング(ResultとEither)

はじめに

諸々の事情で、今後Kotlinを使うことになりようなので、少し学びたいなと思いました。 いろいろ本とか読んでる中で、Javaと大きな違い1つとして、エラーハンドリングのやり方があるように感じたので、このブログではそれについてまとめます。
大本は、みんなのKotlin 現場で役立つ最新ノウハウ!に書かれていたResultEitherを深ぼって自分用にまとめ直したものなので、よりわかりやすくまとめられている本家をみるほうがよいと思います。

ResultとEither

KotlinのエラーハンドリングではResultEitherという型をうまく扱って行なうようです。

Result

Resultは関数の実行の成功と失敗(Success T | Failure Throwable)を表すユニオン型で、Kotlin1.3以降で利用することが可能です。
早速使っていきます。

Resultを使ったエラーハンドリング

例えば以下のような必ず例外を起こすような関数があるとします。

private fun doSomethingButErrorHappen(): String = throw RuntimeException("execute failed")

この関数は以下のようにハンドリングすることができます。

fun main() {
    runCatching { doSomethingButErrorHappen() }
            .onFailure { println(it.message)}
            .onSuccess { println("execution success") }}
}

runCatchingブロックの中で実行された関数の戻り値はResult<T>で包まれて、onFailureonSuccessでそれぞれ関数の成功と失敗(例外発生)をハンドリングすることができます。
main関数を実行すると標準出力に以下の出力がされます。

execution failed

もう少し複雑なパターンとして、とあるリストに対する逐次的な処理でのエラーハンドリングを行いたい場合は以下のようにします。

// 失敗する可能性がある関数
// 引数して3を受け取った場合に例外を投げる
private fun doSomethingsMightErrorHappen(number: Int): String {
    if (number == 3) {
        throw RuntimeException(number.toString())
    }
    return number.toString()
}

// runCatchingを外出しするためのヘルパー関数
private fun doSomethingsCatching(numbers: List<Int>): List<Result<String>> = numbers.map {
    runCatching {
        doSomethingsMightErrorHappen(it)
    }
}

// main
fun main() {
    doSomethingsCatching(listOf(1, 2, 3, 4, 5)).map { result ->
        result.onSuccess { println(it) }
                .onFailure { println(it) }
    }
}

ヘルパー関数のmapないでrunCatchingを呼び出すことによって、Resultのリストを作成し、そのりすとに対して、失敗と成功をハンドリングしています。
こいつの実行結果は以下のとおりになります。

1
2
java.lang.RuntimeException: 3
4
5

単にmap関数を実行すれば成功のケースのみに対して、追加処理を実行することができます。

fun main() {
    doSomethingsCatching(listOf(1, 2, 3, 4, 5)).map { result ->
        result.map { println(it) }
    }
}

実行結果

1
2
4
5

Either

Resultとは別にKotlinのArrowというライブラリを用いると、Eitherという型で自由な成功と失敗を表現することができます。 Eitherそのものは2つの値どちらかになるという中小概念を型として扱うためのものです。
Arrowを使う際はここを参考にプロジェクトに依存を追加してしてください。

Eitherで失敗と成功を表現する

Eitherは以下のように作成することができます。

//成功時のEither
val right = Either.right("success")

//失敗時のEither
val left = Either.left(RuntimeException())

//どちらともなりうる場合のEither
val errorOrVal = Either.cond(Random.nextBoolean(),
        ifTrue = { "success" },
        ifFalse = { RuntimeException() }
)

ドキュメントによると慣習的にrightが成功、leftが失敗を表すそうです。 ここで、失敗を表すleftにはRuntimeExpection()を詰め込んでますが、とくにThrowableじゃないとだめみたいな制約は無く、自由に失敗を表現することができます。

Eitherでエラーをハンドリングする

Eitherでエラーハンドリングを行なう場合パターンマッチで行なう方法とfoldを使う方法のに種類あります。
まずは単純に失敗を返す関数を作成します。

private fun doSomethingButErrorHappenWithEither(): Either<Exception, String> = Either.left(RuntimeException("execute failed"))

上記の関数をパターンマッチでハンドリングすると以下のようになります。

when (val either = doSomethingButErrorHappenWithEither()) {
    is Either.Left -> println(either.a.message)
    is Either.Right -> println("execution success")
}

関数はEither.Leftを返すのでエラーを出力する側にパターンマッチされます。 実行結果は以下の通り

execution failed

こんどはfold関数をつかって同じことをしてみます。

val either = doSomethingButErrorHappenWithEither()
either.fold({ println(it.message)},{ println(it)})

foldを使うことで、より記述が簡潔になりました。
実行結果も同じです。

executio

最後に、とあるリストに対する逐次的な処理でのエラーハンドリングを行ってみます。

private fun doSomethingsMightErrorHappenWithEither(number: Int): Either<Exception, String> {
    if (number == 3) {
        return Either.left(RuntimeException(number.toString()))
    }
    return Either.right(number.toString())
}

ハンドリング側はこんな感じ

listOf(1, 2, 3, 4, 5)
        .map {
            doSomethingsMightErrorHappenWithEither(it)
                    .fold(
                            { f -> println(f.message) },
                            { s -> println(s) }
                    )
        }

実行すると以下のような出力がされました。

1
2
java.lang.RuntimeException: 3
4
5

参考資料