Home About Contact
Kotlin , Maybe

Kotlin で Maybe その3

以前に Kotlin で Maybe その2 普通に Optional を使う というエントリーを書いたのだが、その後、kotlin で Generics を使った Maybe を使うと より自然に違和感なく 値があったりなかったりする値 を扱う状態をコードで表現できることがわかったので、それを書き残しておく。

なお、もっと以前に Kotlin Sealed クラスを使った Maybe の実装というエントリーでも Generics な Maybe を書いていたのだが、そこから多少改良した。

Pokemon をプログラムで扱いたいとする。

data class Pokemon(val name: String, val kind: String)

Pokemon がない場合もあるので、MaybePokemon を定義する。

sealed class MaybePokemon {
    data class Just(val value: Pokemon): MaybePokemon()
    object Nothing: MaybePokemon()
}

これはこれで問題ないのだが、もし Pokemon ではなく、別のなにかを Maybe にしたいときに (たとえば Mushiking とか?) MaybeMushiking というほとんど MaybePokemon と同じクラスを定義しなければならない。

このように Maybe なんとかクラス増殖問題を避けるために Generics な Maybe を定義する。 以前はこのような Maybe を定義して使っていた。

sealed class Maybe {
    data class Just<T>(val value: T): Maybe()
    object Nothing: Maybe()
}

これを改良して次のように定義した。

sealed class Maybe<in T> {
    data class Just<T>(val value: T): Maybe<T>()
    object Nothing: Maybe<Any>()
}

それではこの改良版 Maybe を使用する例を見る。

sealed class Maybe<in T> {
    data class Just<T>(val value: T): Maybe<T>()
    object Nothing: Maybe<Any>()
}

data class Pokemon(val name: String, val kind: String)

val createPokemonDb: ()->List<Pokemon> = {
    val db = arrayListOf<Pokemon>()
    db.add(Pokemon("Eevee",      "normal"))
    db.add(Pokemon("Pidgeot",    "normal"))
    db.add(Pokemon("Pikachu",    "electric"))
    db.add(Pokemon("Squirtle",   "water"))
    db
}

val findPokemonByName: (List<Pokemon>, String)-> Maybe<Pokemon> = {pokemonDb, name->
    val pokemons = pokemonDb.filter { it.name==name }
    if( pokemons.size>0 ){
        Maybe.Just(pokemons.first())
    } else {
        Maybe.Nothing
    }
}


val db = createPokemonDb()

val result = findPokemonByName(db, "Pikachu")
//val result = findPokemonByName(db, "Golduck")

val name = when(result){
    is Maybe.Just -> { result.value.name }
    else -> { "No Pokemon Found" }
}
println( name )

kotlin main.kts で実行して作動を確かめる。

$ kotlin main.kts
Pikachu

Golduck の行をイキにして実行すると次のようになる。

$ kotlin main.kts
No Pokemon Found

ちなみに version はこれ:

$ kotlinc -version
info: kotlinc-jvm 2.0.10 (JRE 17.0.12+7-Ubuntu-1ubuntu224.04)

ここまでは、 ポケモン名で検索して該当ポケモンを Maybe<Pokemon> で返す場合に Maybe を使用した。 別の使用例として たとえば、Trainer のポケモンをセットして使いたいが、初期状態ではポケモンはなにもセットされていない、という状態を表現する 場合を考える。

次のように記述できる。

class Trainer(val name: String){
    var maybePokemon: Maybe<Pokemon> = Maybe.Nothing

    override fun toString(): String{
        return "${name} : ${maybePokemon}"
    }
}

これを使う。

val satoshi = Trainer("Satoshi")
println( satoshi )

// ここでポケモンをセット
satoshi.maybePokemon = findPokemonByName(db, "Pikachu")
println( satoshi )

実行すると次のようになる。

$ kotlin main.kts
Satoshi : Main$Maybe$Nothing@1146e32e
Satoshi : Just(value=Pokemon(name=Pikachu, kind=electric))

まとめ

このように Generics な Maybe を使えば、 値が無い場合もある 状態を自然に表現できる。

なお、別に普通に null 使えばいいんじゃないの?という話もある。 Trainer クラスの例で言えば、このように。

class Trainer(val name: String){
    var pokemon: Pokemon? = null
}

これで事足りるので、それでも var maybePokemon: Maybe<Pokemon> を使いたい理由はなんだろう。 タイプ量は増えるがコードの可読性が上がることだろうか。