前回書いたこれを同僚に「こんな感じだったよ。勉強になったよー!」って見せたら「なるほど勉強になった!ありがとう!」って言ってもらえたのでよかった。
そしてその同僚が「こういうのってJavaだったらどう書くの?」って言って「んー。最近Java書いてないからどうだっけなぁ・・・そもそもJavaのときには、Discriminated Unionみたいなの欲しいと思ったことなかった気がするなぁ」ってなったのでJavaで考えてみることにした。
前回の記事のTypeScriptのコード
type MyUnion = | { tag: "a", value: boolean } | { tag: "b", value: string } const values: MyUnion[] = [ { tag: "a", value: true }, { tag: "b", value: "foo" }, ] type MyMapping = { [I in MyUnion as I["tag"]]: I["value"] } function getValue<T extends MyUnion["tag"]>(t: T): MyMapping[T] { for (const v of values) { if (v.tag === t) { return v.value as MyMapping[T]; } } throw new Error("Unknown tag."); }
このコードでやりたいことは「タグ付きユニオンのタグを指定して、型安全に値を取り出したい」ということ。結局最後はType Assertionを使ってしまった。
Javaで考えてみる
だいぶ久しぶりのJava
だいぶ久しぶりにJavaを触ったので思い出したりしながら考える。もうバージョン23かー!
今なら println(...) だけでもいけるって @sugarlife さんに教えてもらったのでやってみたらいけた!みじかいー!ありがとうございます! https://t.co/2n6KfX2ipu pic.twitter.com/kVMww4L3Id
— SHIIBA Mitsuyuki (@bufferings) 2024年9月22日
そもそもを考える
そもそも「タグ付きユニオンのタグを指定して、型安全に値を取り出したい」をJavaで考えるとどうなるっけ?
- 「タグ付きユニオン」→「インターフェースを実装したクラス」で表現しそう。
- ってことはTSのときに
"a"
や"b"
を文字列ではなく型として渡してた部分はA.class
やB.class
でいいか。 - つまり「インターフェースの実装を持ったリストから
A.class
やB.class
を指定して値を型安全に取り出す」ってことかなぁ。
と思いながらこんなコードを書いた
sealed interface Mapping<T> { T value(); record A(Boolean value) implements Mapping<Boolean> {} record B(String value) implements Mapping<String> {} } <T> Optional<T> getValue( List<? extends Mapping<?>> values, Class<? extends Mapping<T>> clazz) { return values.stream() .filter(clazz::isInstance) .map(clazz::cast) .findFirst() .map(Mapping::value); } void main() { var values = Arrays.asList( new Mapping.A(true), new Mapping.B("foo") ); var a = getValue(values, Mapping.A.class); println(a.orElseThrow()); var b = getValue(values, Mapping.B.class); println(b.orElseThrow()); }
これでa
はOptional<Boolean>
で、b
はOptional<String>
として取り出せている。
実行するとこうなる
トップレベルでmainを定義したり、System.out
を書かずにprintln
を使ったり、などプレビューフィーチャーも勉強がてら使っているので --enable-preview
をつけて実行。
❯ java --enable-preview hello.java true foo
ちゃんと動いた。よかった。
説明
まず最初にT value();
を持ったインターフェースを定義した。Mapping<Boolean>
はBoolean value()
を持った型になる。
sealed interface Mapping<T> {
T value();
}
↑のインターフェースはSealed Classesにしてみた。別に今回の例ではSealed Classにする必要はないかなーとは思ったんだけど、なんとなく使ってみたかったから使った。わいわい。
ということで↓のようにA
とB
を定義した。これでMapping
の実装クラスはA
とB
だけに限定される。
sealed interface Mapping<T> { T value(); record A(Boolean value) implements Mapping<Boolean> {} record B(String value) implements Mapping<String> {} }
ついでに、このA
とB
はRecord Classesにした。これもただ使いたかっただけ。A
はBooleanの値を持ったクラス、B
はStringの値を持ったクラスになっている。
getValue()
は、ちょっととばしてmain
関数の中。
var values = Arrays.asList( new Mapping.A(true), new Mapping.B("foo") ); var a = getValue(values, Mapping.A.class); println(a.orElseThrow());
Mapping
のリストを作って、その中からA
の値を型安全に取り出す、というのをやっている。
で、そのgetValue()
がこう。
<T> Optional<T> getValue( List<? extends Mapping<?>> values, Class<? extends Mapping<T>> clazz) { return values.stream() .filter(clazz::isInstance) .map(clazz::cast) .findFirst() .map(Mapping::value); }
取り出す値の型をGenericsのType Parameterにしてみた。
clazz
でクラスを受け取って、そのインスタンスをvalues
の中から見つけてきてその型にキャストして最初の値を取得している。
存在しない場合を考慮してOptionalにしたけど、TSの方に合わせるならここでorElseThrow()
してもよかったかもしれない。
そんな感じ
おもしろかった。TypeScriptの柔らかい感じも好きだけど、JavaはJavaで型がしっかりしてていいなーと思った。どっちも好き。