nがひとつ多い。

えぬなおの技術的なことを書いていくとこ。

【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チェックなどをするまでもなく、 StringError が帰ってきた方を仕分けすれば良くなる。 他にも 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),
}

要はジェネリクスenumだって話です。

一方Kotlinでは...

一応実装はあるんですが、

qiita.com

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) }

誤解なきよう、これはこれで便利な訳です

それなら使おうライブラリ

今回はこちらです

github.com

このライブラリを使うことで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という宣伝記事でした。