以前に 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> を使いたい理由はなんだろう。 タイプ量は増えるがコードの可読性が上がることだろうか。