4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

KotlinでRustライクなResult型を使う

Last updated at Posted at 2022-06-07

対象読者

RustのResult型(や、他言語における同等機能)を知らない人。
投げる例外しか扱ったことがないのに、kotlin-resultを使ってくれと言われた人。

例外を投げる方式の問題点

エラーを通知する方法として、例外を投げるという方法を採る言語は多いです。この方法の欠点は、(言語仕様にもよりますが)エラー発生時のインタフェースを適切に設定できないことです。

例えば、

fun doubleIfPositiveOrThrow(input: Int): Int {
    if (input <= 0) {
        throw Error("Invalid input!")
    }
    return input * 2
}

という関数は、引数に整数を取って戻り値として整数を返す、というところまでインタフェース定義できているにも関わらず、例外を投げるかどうかは実装詳細を読む必要があります。このせいで、呼び出し元にエラーハンドリングを強制することができず、バグを生む原因になります。

エラーを例外として扱わず、戻り値として扱う方法

そこで、いくつかの言語・・・特に新しめの言語では、エラーを例外として特例扱いするのではなく、普通に戻り値として扱う方法を採っています。

例えばGoでは、エラーが発生する可能性のある関数の戻り値は、常にerrを含みます。

func Atoi(s string) (int, error)

この Atoi関数の場合、第二戻り値がerrorなので、この関数はエラーが発生する可能性があるのだ、ということが、型を見るだけで分かります。

RustやScalaでは、Goと違ってジェネリクスが使えるため、少し異なるアプローチを採っています。それがResult型です。(※ScalaではEitherと呼びますが、実質同じです。)

これから紹介するKotlinのResultも、それと同じものです。

kotlin-resultライブラリ

では、本命のkotlin-resultライブラリの説明を始め・・・たいところなのですが、その前に注意点です。

KotlinのスタンダードライブラリにResult型というのがありますが、これは全く異なるものです。これはこれでエラーハンドリングに役立ちそうな気配はありますが、今回は触れません。

今回説明するのは、kotlin-resultライブラリResult型です。

関数定義

Resultを使うと、前述の

fun doubleIfPositiveOrThrow(input: Int): Int {
    if (input <= 0) {
        throw Error("Invalid input!")
    }
    return input * 2
}

は、

fun doubleIfPositive(input: Int): Result<Int, String> {
    if (input <= 0) {
        return Err("Invalid input!")
    }
    return Ok(input * 2)
}

と書き換えられます。

まずは、インタフェース(関数の型)だけ見てみます。

変更前のfun doubleIfPositiveOrThrow(input: Int): Intは、「整数を食って、整数を返す」ということを言っています。ただし実は、定義にないエラーが発生することがあり得ます。

変更後のfun doubleIfPositive(input: Int): Result<Int, String>は、「整数を食って、成功したら整数を返す、失敗したら文字列を返す」ということを言っています。(Resultの第一型引数が成功時の型、第二型引数が失敗時の型を表す。)

関数の中身も見てみましょう。

変更前はthrowしていたところが、変更後はreturn Err("Invalid input!")に変わっています。例外として投げてしまうのではなく、あくまで戻り値として返しているわけです。ただ、エラーであることを表現するため、Errで戻り値を囲っています。

最後のreturnですが、変更前はreturn input * 2とIntを返していたところが、変更後はreturn Ok(input * 2)に変わっています。成功したことを表すため、Okで戻り値を囲うことが必要となります。

Resultを返す関数の使い方

例えば、

val result = doubleIfPositive(5)

とした場合、resultの値は10にはなりません。Ok(10)になります。よって、そのままでは使えません。

例えば、以下のように使います。

fun tryResultType() {
    val result = doubleIfPositive(5) // これはOk(10)を返すので、このままではIntとして使えない。

    // 成功なら中身の値(=10)、エラーならnull。
    val doubled0: Int? = result.get()

    // 成功なら中身の値(=10)、エラーなら0。
    val doubled1: Int = result.getOr(0)

    // 成功なら中身の値(=10)、エラーならtryResultType自体から抜けてしまう。
    val doubled2: Int = result.getOrElse { return }
}

以上の使用例を見ると分かるように、どの使い方でも、常にエラーだった場合のことを意識しています。つまり、

fun tryResultType() {
    val result = doubleIfPositive(-3) // これはErr("Invalid input!")を返す。

    val doubled0: Int? = result.get() // null

    val doubled1: Int = result.getOr(0) // 0

    val doubled2: Int = result.getOrElse { return } // tryResultType自体から抜けてしまう。
}

という感じです。

いったんまとめ

まとめると、例外を投げる方式と比べて、

  • インタフェース(関数の型)を見るとエラーが発生しうることが分かる。
  • 関数呼び出し側にエラー処理を強制できる。(→処理忘れによるバグを生みづらい。)

というメリットがあります。

bindingとbind

しかし、いちいちgetOrElseなどを書くのは結構面倒です。

つまり、従来通りの例外投げ方式だと、以下のように書けていました。

try {
    val a: Int = doubleIfPositiveOrThrow(2)
    val b: Int = doubleIfPositiveOrThrow(5)
    val c: Int = doubleIfPositiveOrThrow(-8)
} catch (e: Error) {
    // エラー処理
}

Result方式だと、こうなります。

val a: Int = doubleIfPositive(2).getOrElse {
    // エラー処理
    return
}
val b: Int = doubleIfPositive(5).getOrElse {
    // エラー処理
    return
}
val c: Int = doubleIfPositive(-8).getOrElse {
    // エラー処理
    return
}

これを楽に書けるようにするのが、bindingbindです。

bindingbindを使うと、このように書けます。

binding<Unit, String> {
    val a: Int = doubleIfPositive(2).bind()
    val b: Int = doubleIfPositive(5).bind()
    val c: Int = doubleIfPositive(-8).bind()
}.getOrElse {
    // エラー処理
    return
}

簡単に言えば、こういうことです。

bindingブロックの中では、Result型はbind()を呼び出すことができます。
bind()は、もし呼び出し元のResultが成功(Ok)であれば中身の値を取り出し、失敗(Err)であればbindingブロックから抜けて、エラー(Err)を返します。

楽できるので、使えるところではbindingbindを使った方が良いです。

詳細は、公式ドキュメントを参照してください。

最後に

詳しく説明すると長くなるので、いったんここまでにします。

投げる例外にしか触れてこなかった人にとっては、最初はややこしく感じるかと思います。しかし、投げる例外では、厳密なエラー処理をプロジェクト全体に浸透させるのは難しいです。Resultを使う方法だと、いい加減に書いてしまうことが許されなくなる分、バグの少ないコードを生み出すことができる・・・と思っています。

少なくとも、Rustは言語レベルでそういう選択をしたわけですし。

とにかく、触っているうちに慣れますから!

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?