前回書いたこれを同僚に「こんな感じだったよ。勉強になったよー!」って見せたら「なるほど勉強になった!ありがとう!」って言ってもらえたのでよかった。
そしてその同僚が「こういうのって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で型がしっかりしてていいなーと思った。どっちも好き。