随分以前に Kotlin Sealed クラスを使った Maybe の実装というエントリーを書いたのだが、 Maybe を使う目的が 値があったりなかったりする値 を上手に扱いたい、ということであれば、 わざわざ自前で定義するのではなく、 (Java とか Kotlin の世界では)java.util.Optional を使えばいいじゃないか。
Kotlin には たとえば String? のように型に ? をつけることで null を許容する記述方法があり、 これを使えば実質 java.util.Optional を使うのと同じ効果を得られるような気もしますが、 ここでは、普通に java.util.Optinal を使って、値があったりなかったりする値 を扱う方法を調べていきます。
以前のコードからここで説明したいポイントだけを抜き出したコードを書いてみると次のようになります。
data class Pokemon(val name: String, val kind: String)
sealed class MaybePokemon {
data class Just(val value: Pokemon): MaybePokemon()
object Nothing: MaybePokemon()
}
val getName: (MaybePokemon)->String = { maybe->
when( maybe ){
is MaybePokemon.Just -> maybe.value.name
is MaybePokemon.Nothing -> ""
}
}
// --- main ---
val pokemon1 = MaybePokemon.Just(Pokemon("Pikachu", "electric"))
val pokemon2 = MaybePokemon.Just(Pokemon("Eevee", "normal"))
val pokemon3 = MaybePokemon.Nothing
val maybePokemonList = listOf<MaybePokemon>(pokemon1, pokemon2, pokemon3)
maybePokemonList.forEach {
println( getName(it) )
}
実行するには このコードを main.kts に保存したとして、次のように実行します。
$ kotlinc -script main.kts
バージョンの確認:
$ kotlinc -version info: kotlinc-jvm 1.8.10 (JRE 17.0.9+9-Ubuntu-122.04)
Pokemon という値を扱いたいが、あったりなかったりするので、Maybe にしたい。 そこで MaybePokemon クラスを定義して、コードを書きました。
これで、もしポケモンが ない 値の場合でも if とか when で分岐させないで コードを書いていくことができます・・・ main 部分のコードでは。 裏舞台の getName 関数の内部では、やっぱり when が必要になりますが。
これはこれでよいのですが、 もし、Pokemon の代わりに 別の型・・・たとえば Product を使いたいと思ったときに、MaybeProduct を定義しなければいけません。 このように Maybe として扱いたい型が出現するたびに次々に定義を増やしていくのは当然 いや です。
ならば、Generics を使えばいいじゃないか、ということで、以下のように記述できることはできるのですが・・・気に入らない。 なにが困るかというと genName 関数は Any? しか返せないのです。
もしかすると Generics を上手に定義してやれば、この問題回避できるのかもしれません。 (こちらを参照 Kotlin で Maybe その3)
しかし、後述するように あったりなかったりする値 を扱うために、 Maybe を定義したいのであれば、それを自分で定義することにこだわらずとも、 単に java.util.Optional を使ったほうが楽です。
sealed class Maybe {
data class Just<T>(val value: T): Maybe()
object Nothing: Maybe()
}
val getName: (Maybe)->Any? = { maybe->
when( maybe ){
is Maybe.Just<*> -> maybe.value
is Maybe.Nothing-> ""
}
}
// --- main ---
val pokemon1 = Maybe.Just(Pokemon("Pikachu", "electric"))
val pokemon2 = Maybe.Just(Pokemon("Eevee", "normal"))
val pokemon3 = Maybe.Nothing
val maybePokemonList = listOf<Maybe>(pokemon1, pokemon2, pokemon3)
maybePokemonList.forEach {
println( getName(it) )
}
確かに、名前を標準出力するだけなら getName 関数の返す値の型が Any? だとしてもたいして問題はないのですが...
追記: というか、単に getName 関数を次のように定義すればよいだけなのでは?
fun getName(maybe: Maybe): String { return when( maybe ){ is Maybe.Just<*> -> "${maybe.value}" is Maybe.Nothing-> "" } }
自前の Maybe を定義するのではなく、 Optional を使ってこのコードを書きかえてみます。
import java.util.Optional
data class Pokemon(val name: String, val kind: String)
val getName: (Optional<Pokemon>)->String = { pokemonOpt->
if( pokemonOpt.isPresent() ){ pokemonOpt.get().name } else { "" }
}
// --- main ---
val pokemonOpt1 = Optional.of(Pokemon("Pikachu", "electric"))
val pokemonOpt2 = Optional.of(Pokemon("Eevee", "normal"))
val pokemonOpt3: Optional<Pokemon> = Optional.empty()
val pokemonOptList = listOf(pokemonOpt1, pokemonOpt2, pokemonOpt3)
pokemonOptList.forEach { pokemonOpt->
println( getName(pokemonOpt) )
}
存在しないポケモンの値を定義するときに次のように書いています。
val pokemonOpt3: Optional<Pokemon> = Optional.empty()
単に次のように書くと、コンパイルできないので注意しましょう。
val pokemonOpt3 = Optional.empty()
これで getName 関数は String を返すことができるようになりました。
Maybe って言いたいだけならば(なんの意味が・・・)、 typealias することで Optional を Maybe にできる。
typealias Maybe<T> = Optional<T>
そうすれば Maybe を使ってコードがかける。
import java.util.Optional
typealias Maybe<T> = Optional<T>
data class Pokemon(val name: String, val kind: String)
val getName: (Maybe<Pokemon>)->String = { maybePokemon->
if( maybePokemon.isPresent() ){ maybePokemon.get().name } else { "" }
}
val maybePokemon1 = Maybe.of(Pokemon("Pikachu", "electric"))
val maybePokemon2 = Maybe.of(Pokemon("Eevee", "normal"))
val maybePokemon3: Maybe<Pokemon> = Maybe.empty()
val maybePokemonList = listOf(maybePokemon1, maybePokemon2, maybePokemon3)
maybePokemonList.forEach { maybePokemon->
println( getName(maybePokemon) )
}
Optional の別名定義しただけなので、かえって混乱を招くだけな気がします。 とはいえ、 maybePokemon のような変数名は確かに通りはいいですね。
Java (Kotlin) の世界観に沿ったコードを書こう。