Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

Kotlin のエラーハンドリングは Either 使っておけばいいざぁ

Java もなかなか良い言語だと思いますが、 Android だとやはり Kotlin のほうが優勢な気がしている AIR Design for Apps 事業部 Android エンジニアの松下です。

最近 Kotlin でのエラーハンドリングについて色々考えているので、それについてつらつらと書いてみようと思います。

Kotlin のエラーハンドリングについて

まず、 Kotlin には Java と違って検査例外が存在しません。おさらいですが、検査例外とは以下のようなものです。

// java
class HogeException extends Exception {}

class Hoge {
    public static void hoge() throws HogeException {
        throw new HogeException();
    }

    public static void main(String[] args) {
        new Hoge().hoge(); // error: unreported exception HogeException; must be caught or declared to be thrown
    }
}

try-catch するか、 throws で更に上に投げるかをコンパイル時に精査して、していない場合はコンパイルエラーにします。
この機能の使われ方の是非はともかくとして、 まあそれなりにフールプルーフっぽい機能です。

ご存知の通り、AltJava である Kotlin や Scala にはこの機能は存在しません。なので、以下のようなコードを書くと実行時エラーとなります。

// kotlin
class HogeException: Exception()

fun hoge() {
    throw HogeException()
}

fun main(args: Array<String>) {
    hoge()
}

実行時エラーを防ごうとすると、エラーを投げられていないか?というのを目視で確認しなければなりません。まあこの程度の規模なら全然問題はないのですが、 interface などで包まれていると、その interface を実装しているクラスを見て回らないといけなかったり、大規模になるとそれだけで結構時間が持っていかれてしまいます。

やっぱり、実行時エラーよりコンパイルエラーのほうが気持ち的には楽です。特定の操作時のみエラーで落ちるとか…嫌じゃないですか。

Either とは

(2つのうちの)どちらか一方の
Weblio より引用
https://ejje.weblio.jp/content/either

という意味のようです。

Either関数型プログラミング言語でモナドとかいうなにかの一部のようです。まあこのあたりは今回は説明しません。できません。*1

Scala だと標準ライブラリに入っています。 Kotlin だと Arrow のような関数型プログラミング支援ライブラリを使うことになると思いますが、 Either だけ使いたいならこれはちょっと大きすぎるような気がしているので、考え方だけ持ち帰って貰えれば大丈夫です。

関数型プログラミング言語における Either も、先述の通り、 2 つのうちどちらかを返すものです。

(以下、 Scala のコードが混じりますが、 Kotlin のコードも乗せておきます。)

// scala
Either[HogeException, Int]
// kotlin
Either<HogeException, Int>

であれば、 HogeExceptionInt のどちらかを表す型ということになります。
具体的には以下のように使います。

// scala
val a: Either[HogeException, Int] = Right(123)
val b: Either[HogeException, Int] = Left(new HogeException())
// kotlin
val a: Either<HogeException, Int> = Right(123)
val b: Either<HogeException, Int> = Left(HogeException())

Either では右が正しいものとする慣習があるようなので、正常系を右、異常系を左とすると混乱しないと思います。*2

値を取り出すときはダウンキャストなどをして取り出します。

// scala
val a: Either[HogeException, Int] = Right(123)

a match {
    case Right(i) => println(i)
    case Left(e)  => println(e)
}
// kotlin
val a: Either<HogeException, Int> = Right(123)

when(a) {
    is Right -> println(a.value)
    is Left  -> println(a.value)
}

このように、値を取り出す前にはエラーハンドリングを型で半強制できるというのが Either の強みです。(もちろん、 case Left(e) => throw e とかしたら意味ないですよ)

(他にも、 flatMap とかで処理をつなげたりできますが、ここでは省略します。)

でも、 try-catch でよくない?

もちろん、 try-catch もサクッと使えて良いと思います。
しかし、どれを catch すればよいのか、そもそもどの処理を catch する必要があるのか、というのに悩んではいませんか。
結局めんどくさくて、 catch(e: Exception) みたいになってしまっていませんか。

そう、Kotlin の try-catch は型安全ではないのです。どのエラーが発生しうるか、コンパイル時にはわかりません。
一方 Either でのエラーハンドリングは型安全です。どのエラーが発生しうるかを型で表現することができます。
静的型付け言語の強みを活かして、安全かつ高速に開発を進めたいと思っているのであれば、少しだけ、勉強してみると良いかもしれません。

Result について

そういえば Kotlin 1.5 から Result が戻り値に指定できるようになりました。

Either と違って、異常系の型を指定することはできません( Throwable で固定)が、エラーが発生しうるというのを呼び出し側に伝える用途としては十分使えると思います。

ライブラリを導入するよりは、だいぶハードルが低いはずなので、興味があればこれも触ってみると良いと思います。

最後に

弊社ではいっしょに働く Android エンジニア、iOS エンジニアを募集しています!
ご興味のある方はぜひご応募いただけますと嬉しいです。

Androidエンジニアの方はこちらから: www.wantedly.com
iOSエンジニアの方はこちらから: www.wantedly.com

腕に覚えのある方はリードエンジニアの求人もありますので是非ご検討ください。AndroidiOSの両方の開発ができるエンジニアも大歓迎です! www.wantedly.com

*1:とりあえず、ゼノブレイドは関係ないようです。

*2:クラピカ理論は関係ないようです。