あの日見たジャバの最新バージョンを僕達はまだ知らない。
こんにちは、びしょ~じょです。 気づけば4月になっていました。 で?
Well done! Your session Guide to Profile-Guided Optimization: inlining, devirtualining, and profiling has been accepted for Go Conference 2024!
— びしょ〜じょ (@Nymphium) April 4, 2024
PGOと周辺の最適化の真実についてお話します。6月8日は渋谷アベマタワーズでボクと握手
ありがとう! GoConference2024はオフライン開催です!!
ということで今日はジャバの話です。 逆張っていくわけではなくて、前々から書こうと思っていたが執筆中にacceptのメールが来たんで、人生はタイミングです。
1. はじめに
あなたがちょうど10年ほど前T大1の情報科学類生だったとき、『データ構造とアルゴリズム』という講義を受けてましたね2。 そこで使われていた言語は? そう、ジャバ。 2013年のジャバの最新バージョンは7でした。 ときは流れ2024年、あなたはご存知ですか。ジャバの最新バージョンを。 なんと…22です!
今回は15バージョン分の遅れを取り戻していく。
2. 題材
今回はこちら
とりあえずラムダ計算を実装していく。 例によってパーザは実装していない。
まあ、ジャバの前ではなんでもいいですわ。
3. Syntax - Sealed Classes, Records, String Templates
昔のジャバなら次のようにinterfaceをclassが継承していくようになるだろう(図3.1)。
// V for variable, ord and eq
public interface Lambda<V> {
// しれっとMapではなくてListにしているがご容赦してくれい
public Lambda<V> eval(List<Pair<V, Lambda<V>>> env);
}
public class Abs extends Lambda<V> {
V param;
Lambda<V> body;
Abs<V>(V param, Lambda<V> body) {
this.param = param;
this.body = body;
}
public Lambda<V> eval(List<Pair<V, Lambda<V>>> env) {
// ......
}
}
この方法には問題がある。
このinterfaceを継承するclassは際限なく増えてしまう。
このため、eval
メソッドのディスパッチ時に不明なクラスが出てくる可能性がある。
そこで、ver17より導入されたSealed Classesを使う(図3.2)。
public sealed interface Lambda <V>
permits Var, Abs, App {
public Lambda<V> eval(List<Pair<V, Lambda<V>>> env);
}
ワーオ! これでLambda
を継承するclassはVar
、Abs
、App
に絞ることができるんですね。
public class Abs extends Lambda<V> {
V param;
Lambda<V> body;
Abs<V>(V param, Lambda<V> body) {
this.param = param;
this.body = body;
}
// ......
こちらver16で導入されたRecordsを使うと簡単に書ける(図3.3)。
public record Abs<V>(V param, Lambda<V> body) implements Lambda<V> {
// ......
}
ワーオ! 便利ですね。
name
がprivate final
になり、getter、equals
、ちょっといい感じのtoString
がパラメータに対して自動生成される(図3.4)。
jshell> new Abs("x", new Var("x"))
| Warning:
| unchecked call to Var(V) as a member of the raw type Var
| new Abs("x", new Var("x"))
| ^----------^
| Warning:
| unchecked call to Abs(V,Lambda<V>) as a member of the raw type Abs
| new Abs("x", new Var("x"))
| ^------------------------^
// いい感じのtoString
$5 ==> Abs[param=x, body=Var[name=x]]
toString
が定義されていれば生成されない。
今回はpretty printのために手で書く(図3.5)。
public record Abs<V>(V param, Lambda<V> body) implements Lambda<V> {
public String toString() {
return "λ" + param + "." + body;
}
// ......
}
ところでよぉ、モダンジャバってのにはstring interpolationもねえのかよ。 あるんだよね。 ver22でPreviewとしてString Templatesという機能が導入されている(図3.6)。
public String toString() {
return STR.`λ\{this.param}.\{this.body}`;
}
このSTR
ちゅうのがtemplate processorと呼ばれるもので、デフォルトでimportされている3。
このtemplate processorが、渡される文字列の\{expr}
をstringified exprとして埋め込んでくれる。
FMT
というtemplate processorも提供されており、こちらはformat specifierが使える。
4. Eval - Switch Expressions, Pattern Matching
さあ構文木も定義できたしeval
いくぞ。
Var
はさくっと行く。
Var
の評価時に環境から評価する値を取り出すときにver8から導入されたStream APIを使う(図4.1)。
もちろん同時期に入ったLambda Expressionを前提としている。
ver8は2014年だから他の講義で使ってたわ流石に。
public record Var<V>(V name) implements Lambda<V> {
// ......
public Lambda<V> eval(List<Pair<V, Lambda<V>>> env) throws RuntimeException {
// これね
var e = env.stream().filter(p -> p.first().equals(this.name)).findFirst();
if (e.isPresent()) {
return e.get().second();
} else {
throw new RuntimeException(STR."var \{this.name} not found");
}
}
}
ってvar
ってナンデスカー!?
これはver10で導入されたLocal Variable Type Inferenceである。
もうFoobar x = new Foobar()
なんてアホみたいなこと書かんでええのや。
しかし、型推論ができるのはローカル変数のみであり、メソッドのシグネチャ等には使えないためちょっとだけかゆいところに手が届かない。
とはいえ学部時代のジャバからは大進化ですよ。
まあ見るべきはApp
っしょ(図4.2)。
public record App<V>(Lambda<V> func, Lambda<V> arg) implements Lambda<V> {
// ......
public Lambda<V> eval(List<Pair<V, Lambda<V>>> env) throws RuntimeException {
var func = this.func.eval(env);
var arg = this.arg.eval( env);
// switch expression - Java 14
return switch (func) {
// pattern matching - Java 21
case Abs(V param, var body) -> {
var newEnv = new ArrayList<>(env);
newEnv.add(new Pair<>(param, arg));
yield body.eval(newEnv);
}
case NativeFn(Function<Lambda<V>, Lambda<V>> fn) -> fn.apply(arg);
default -> { throw new RuntimeException(STR."not a function: \{func}"); }
};
}
おいなんだこれ! 知ってるジャバじゃねえぞ!! まず、ver14でSwitch Expressionsが導入された。 あのなぁ、ジャバはポリモーフィズムがあってカプセル化があるオブジェクト指向の真の継承者なんだぞ。 なんだよswitch expressionって…。 しかもver21でPattern Matchingも導入された。 もはや関数型言語じゃないですか! オブジェクト指向で関数型…これは……オーキャモ…? …まあ大変便利です、ありがとうモダジャバ4。
Switch expressionはcase内でyield
を使うことで値を返すことができる。
パターンマッチングは言わずもがな、しかし型注釈が必要になるので注意。
5. main - Implicityly Declared Classes and Instance Main Methods
あとはmainから呼ぶだけ。 ここにも驚きがあるのがモダンジャバ(図5.1)。
void main() {
var id = new Abs("x", new Var("x"));
System.out.println(id);
}
なんすかこれはpublic static wow wow yeah void main(String[] args)
じゃねえの?
クラス定義はどこ行ったんスカ?
これはver21からpreviewで入っているImplicityly Declared Classes and Instance Main Methods5による恩恵。
まずmain
が簡潔に書けるようになった。
public
およびstatic
プロパティが緩和され、コマンドライン引数を受け取るargs
も省略可能になった。
次にクラスなしについて。
トップレベルクラス定義がないと無名メソッドが生成され、そのインスタンスメソッドとしてmain
が呼ばれるようになった。
これがstatic
なくてOKな理由である。
Implicitly declared classesは主に、ver10で導入されたLaunch Single-File Source-Code Programsによるスクリプティングを、より簡単におこなうためのものである。
サク書き用ですね。
なので、package
指定が無い場合にのみ使うことができ、一方jarに固めたりするときはエンドポイントを指定できないのでclassは変わらず必要になる。
6. おわりに
Javaのモダナイゼーションにウズウズしてしまい、ver22のリリースでとうとう爆発した結果、筆をとることとなった。 ver21で導入されたVirtual Threadsも触りたいが今回は見送る。 ver22の新機能は主に[6]を参考にした。