はじめに
パターンマッチングは、値が特定の構造を持つかどうかをチェックし、成功した場合にその構成要素を抽出する仕組みです。
主に関数型言語において実装されていましたが、近年では Ruby, Python, Java などのオブジェクト指向言語にも取り入れられています。
断り書き
個人的にパターンマッチングに興味があり、最近 Scheme と Java を初めたので Java のパターンマッチングについてまとめた記事です。
Java にまだ慣れていないのでサンプルコードは Java の記事にも関わらず Scheme(Gauche)、 Python を使っていますが、追々 Java に書き換えていくつもりです。
Java の switch 文・式
Java は C 言語風の switch 文を最初から持っていたが、面白いことに switch 式が追加された。
- Java SE 12(プレビュー):JEP 325 で switch 式を初導入
- Java SE 14(標準機能化):JEP 361 で正式サポート
- Java SE 17(プレビュー):JEP 406 で switch のパターンマッチングを初導入
- Java SE 21(標準機能化):JEP 441 で正式サポート
これによって Java は
- switch 式の便利さ
- 構成要素を抽出する分解
を得た。
switch 式
下記は 1~12 または "春", "夏", "秋", "冬"を入力すると対応する季節か月のリストを返す処理を switch 式で書いたもの。
Object input = "夏"; // ← ここを 1~12 または "春", "夏", "秋", "冬" に変えて試してください
Object result = switch (input) {
case Integer month when month >= 1 && month <= 12 -> switch (month) {
case 3, 4, 5 -> "春";
case 6, 7, 8 -> "夏";
case 9, 10, 11 -> "秋";
case 12, 1, 2 -> "冬";
default -> "不明";
};
case String season -> switch (season) {
case "春" -> java.util.List.of(3, 4, 5);
case "夏" -> java.util.List.of(6, 7, 8);
case "秋" -> java.util.List.of(9, 10, 11);
case "冬" -> java.util.List.of(12, 1, 2);
default -> java.util.List.of();
};
default -> "不明な入力";
};
System.out.println(result);
//[6, 7, 8]
単純な数字だけではなく、型で分岐することができる。
式・文
switch 文が式になったと言ったが、その利点を説明するためには文と式の違いを理解する必要がある。
一言で言うと式は値を返す
文と式の違いを示すため、他の言語を例に出す。
例えば Python にもパターンマッチ機能があるが、match 文という。
# ネストしている配列を平滑化(ネストを排除)する関数
def flatten(lst:list) -> list:
def loop(atom, acc):
match atom:
# 空リスト
case []:
return acc
# リスト
case [head, *tail]:
acc = loop(head, acc)
return loop(tail, acc)
# 非リスト
case _:
return acc + [atom]
return loop(lst, [])
if __name__ == "__main__":
print(flatten([1,[2,3],4]))
# [1, 2, 3, 4]
このようにパターンマッチは文でも便利だが、実は関数の引数にシュっと差し込むようなことはできない。
式の強力さ
式は関数に渡すことができる。Scheme を例に出す。
(display (let loop ((x 10) (acc '())) (if (= x 0) acc (loop (- x 1) (cons x acc)))))
;; (1 2 3 4 5 6 7 8 9 10)#<undef>
このように、関数の引数の中でネストさせることが容易にできる。
;; 1-10 の数を偶奇に分類する
(display
(let loop ((x 10)
(even '())
(odd '()))
(match x
[0
(list (cons 'even (reverse even))
(cons 'odd (reverse odd)))]
[_
(if (even? x)
(loop (- x 1) (cons x even) odd)
(loop (- x 1) even (cons x odd)))]))
)
;; ((even 10 8 6 4 2) (odd 9 7 5 3 1))#<undef>
ロジックを組み込み、偶数・奇数に分類するといった多少込み入った処理も簡単に表現できる。
分解
パターンマッチによって構成要素を引き回すことができる。
;;;; 二分木を走査するプログラム
(define tree1 '(5 (4 () ()) (7 () ())))
;; パターンマッチで巡回
;; 葉の数字を抜き出す
(define (tree-match tree)
(flatten (match tree
[() '()]
[(A B C)
(list A (tree-match C) (tree-match B))])
))
(display (tree-match tree1))
;; (5 7 4)
;;;; Gauche 版 flatten
(use util.match)
(define (flatten lst)
(let loop ((atom? lst)
(acc '()))
(match atom?
[(? null? atom?) acc]
[(? pair? atom?)
(loop (car atom?)
(loop (cdr atom?)
acc))]
[_ (cons atom? acc)])))
(display (flatten '(1 (2 3) (4 (5 6)))))
;; => (1 2 3 4 5 6)#<undef>
このように、パターンマッチは構造化されたデータを直感的かつ宣言的に処理する強力なツールとなる。
終わりに
本記事では、「式と文の違い」「パターンマッチによる構造の分解」「Java における switch の式化とパターンマッチの進化」について概観した。
Java においては、switch
が式になったことで、関数型言語に近い表現が可能になりつつある。加えて、instanceof
の改良やガード付きパターンなど、Java の進化はまだ途中でありながら、確実に「表現力」を手にしつつある。
後でやること
- 変化が起きているので、キャッチアップしたら switch の構文についての説明を追加
- 9 月にリリースされる Java SE 25 が LTS
- サンプルコードが Scheme, Python であるので追々修正する
-
instanceof
パターンマッチ