Home About Contact
Kotlin , Haskell , Functional Programming , Maybe

Kotlin Sealed クラスを使った Maybe の実装

Kotlin の Sealed クラスを使えば、 代数データ型の直和型を表現できる、という情報を得たので、MaybePokemon クラスをつかって試してみた。

main.kt にコードを書いていきます。

ポケモンデータクラスを用意:

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
}

このポケモンデータベースを使ってポケモンをその名前から引けるように findByName 関数を追加:

val findByName: (List<Pokemon>, String)->Pokemon? = {pokemonDb, name->
    val pokemons = pokemonDb.filter { it.name==name }
    if( pokemons.size>0 ){ pokemons.first() } else { null }
}

データベースに存在しないポケモン名が指定された場合に備えて、戻り値は Pokemon? にしています。(Pokemon または null)

これらを使用する main 関数を定義:

fun main(){
    val db = createPokemonDb()
    val result = findByName(db, "Pikachu")
    println(result)
}

Pikachu で検索しています。

実行してみましょう。

$ kotlinc main.kt -include-runtime -d main.jar
$ java -jar main.jar
Pokemon(name=Pikachu, kind=electric)

うまくピカチュを検索できました。

もし存在しないポケモン名で検索すると...

val result = findByName(db, "Golduck")
println(result)

結果は当然 null になります。

一旦、ここまでを整理。 現在の main.kt は以下の通り。

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 findByName: (List<Pokemon>, String)->Pokemon? = {pokemonDb, name->
    val pokemons = pokemonDb.filter { it.name==name }
    if( pokemons.size>0 ){ pokemons.first() } else { null }
}

fun main(){
    val db = createPokemonDb()
    //val result = findByName(db, "Pikachu")
    val result = findByName(db, "Golduck")
    println( result )
}

findByName が null を返すかわりに MaybePokemon を返すようにしてみましょう。 Sealed クラスを使った MaybePokemon は以下の通り:

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

つまり MaybePokemon 型は Nothing | Just Pokemon というような意味の型になります。

これを使うように findByName 関数を修正:

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

実行してみましょう。

Pikachu で検索した場合:

$ kotlinc main.kt -include-runtime -d main.jar
$ java -jar main.jar
Just(value=Pokemon(name=Pikachu, kind=electric))

Just が返る。

Golduck で検索した場合:

$ kotlinc main.kt -include-runtime -d main.jar
$ java -jar main.jar
MaybePokemon$Nothing@610455d6

Nothing になる。

ちなみに、戻ってきた MaybePokemon が Just か Nothing かで処理を分岐する場合は、 たとえば when を使えば以下のように書ける。

val resultMaybe = findByName(db, "Pikachu")

val result: Any = when(resultMaybe) {
    is MaybePokemon.Just -> resultMaybe.value
    is MaybePokemon.Nothing -> ""
}

Just の場合は Pokemon 型、Nothing の場合は 文字列型が来るというちょっと気持ち悪いコードですが。

コード全体 main.kt:

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

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

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 findByName: (List<Pokemon>, String)-> MaybePokemon = {pokemonDb, name->
    val pokemons = pokemonDb.filter { it.name==name }
    if( pokemons.size>0 ){ MaybePokemon.Just(pokemons.first()) } else { MaybePokemon.Nothing }
}

fun main(){
    val db = createPokemonDb()

    val resultMaybe = findByName(db, "Pikachu")
    //val resultMaybe: MaybePokemon = findByName(db, "Golduck")

    val result: Any = when(resultMaybe) {
        is MaybePokemon.Just -> resultMaybe.value
        is MaybePokemon.Nothing -> ""
    }
    println( result )
}

まとめ

Sealed クラスを使うとたとえば、Haskell の Maybe 的なものを表現できる。 とはいえ、この実装では Pokemon 型以外を Maybe にしようとしたら MaybeFoo, MaybeBar, MaybeHoge のように 次々実装していかなければいけない。 このままではあまり実用性はない。

追記 ジェネリックを使えば...

Pokemon 型以外を Maybe にしようとしたら Maybeなんとかクラス増殖問題を回避するにはジェネリクスを使えばいいのかも。

MaybePokemon を:

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

Maybe に変更:

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

それに合わせて findByName も修正:

元の findByName:

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

修正後の findByName (MaybePokemon を Maybe に変更):

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

あとは実際に使うときに when で Just か Nothing か判定する部分を以下のように変更:

val result: Any? = when(resultMaybe) {
    is Maybe.Just<*> -> resultMaybe.value
    is Maybe.Nothing -> ""
}

修正後のコード全体:

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

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

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

fun main(){
    val db = createPokemonDb()

    val resultMaybe = findByName(db, "Pikachu")
    //val resultMaybe: Maybe = findByName(db, "Golduck")

    val result: Any? = when(resultMaybe) {
        is Maybe.Just<*> -> resultMaybe.value
        is Maybe.Nothing -> ""
    }
    println( result )
}

これで、Pokemon 型以外でもどんな型も Maybe にすることができるようになった。

Makefile をメモしておきます:

run: main.jar
	java -jar main.jar

main.jar: main.kt
	kotlinc main.kt -include-runtime -d main.jar

以上です。