2016/02/23 コメントを基に修正。
http://openjdk.java.net/jeps/305 をテキトーに訳した。
JEP 305: Pattern Matching
Author Brian Goetz Owner Gavin Bierman Created 2017/05/30 19:48 Updated 2017/06/16 16:25 Type Feature Status Candidate Component specification/language Scope SE Discussion amber dash dev at openjdk dot java dot net Priority 3 Reviewed by Mark Reinhold Issue 8181287
Summary
Java言語をパターンマッチング(pattern matching)による機能強化を行います。初回のサポートは、switch
ステートメントとmatches
式による、型テスト(type-test)と定数パターン(constant patterns)になります。パターンの適用範囲と言語でパターンマッチングをサポートする要素の拡張は、以降の開発で行います。
Motivation
おおむね大半の言語は、ある式が特定の型や構造を持つことのテストと組みわせる一連のロジックがあり、その次に、以降の処理で使うために状態のコンポーネントを条件付きで展開します。たとえば、Javaではinstanceof-and-castイディオムが良く知られています。
if (obj instanceof Integer) { int intValue = ((Integer) obj).intValue(); // use intValue }
ここには三種類の処理があります。test
(xはIntegerである)、conversion(objをintegerにキャスト)、destructuring(IntgerからintValueコンポーネントを抽出)。このパターンは単純でありJavaプログラマには馴染み深いものですが、最適とは言い難い理由がいくつかあります。まず冗長さで、型テストとキャストはやることが被っています(instanceofしたあと他に何をやるというのだろうか?)。キャストとdestructuringという突然現れるボイラープレートにより、それに続く重要なロジックが見辛くなります。しかし最も甚大なのは、ポイラープレートの繰り返しがプログラムにエラーを気付かれないまま紛れ込ませる可能性がある点です。
複数のターゲット型がありうる場合をテストする場合にこの問題は更に悪化します。上述の例をif...else
テストチェーンに書き加えます。
String formatted = "unknown"; if (obj instanceof Integer) { int i = (Integer) obj; formatted = String.format("int %d", i); } else if (obj instanceof Byte) { byte b = (Byte) obj; formatted = String.format("byte %d", b); } else if (obj instanceof Long) { long l = (Long) obj; formatted = String.format("long %d", l); } else if (obj instanceof Double) { double d = (Double) obj; formatted = String.format(“double %f", d); } else if (obj instanceof String) { String s = (String) obj; formatted = String.format("String %s", s); }
上のコードは見慣れたものですが、多数のよろしくない要素が含まれています。既に述べたように、こうした繰り返しコードはプログラマを苛立たせます。また、ビジネスロジックはポイラープレートの中にたやすく埋もれてしまいます。しかしより重要なのは、このやり方はコーディングエラーを残したままにしてしまう点で、その理由は過剰な制御構造になっているためです。上のコードの意図は、if...else
チェーンの各箇所でformatted
変数に何らかの値を代入することです。しかし、ここで実際に発生することをコンパイラで検証可能にする術がありません。もし、あるブロックが、実際上滅多に実行されないブロックだとして、formatted
への代入を忘れているとバグになります(blank localもしくはblank finalでformatted
にしておけば少なくとも"確実な代入"分析には入れられますが、常に行われるわけではありません)。最後に、上のコードは最適化の余地が限られています。コンパイラの英雄が不在で、基底のプログラムはおおむねO(1)
なのに、O(n)
の複雑さを抱えています。
Description
アドホックな解決策に頼るより、Javaにもパターンマッチング(pattern matching)を入れる時が来たと我々は判断しています。パターンマッチングは1960sにさかのぼる事プログラミング言語でさまざまな異なるスタイルに適用されてきた技術で、SNOBOL4とAWKなどのテキスト指向言語、HaskellとMLなどの関数型言語、最近ではScalaとC#などのオブジェクト指向言語にも拡張されています。
パターン(pattern)とはターゲットに適用可能な述語(predicate)の組み合わせです。述語はバインド変数(binding variables)と共に使用し、もし述語が適用される場合はターゲットから抽出されたものがバインド変数になります。バインディングパターンの形の一つは型テスト(type test)パターンで、以下のようなものです(matches
演算子は概念的なものです)。
if (x matches Integer i) { // can use i here }
Integer i
というフレーズが型テストパターンです。i
は新規の変数宣言で、宣言済みの変数ではありません。ターゲットはInteger
インスタンスかどうかテストされ、次に、Integer
にキャストされてバインド変数i
にint
コンポーネントがバインドされます。
先に触れたように、if...else
の連続は過度に制御構造を多用しているので望ましくありません。Javaには既にswitch
というタコ足(multi-armed)な等価テストの機構があります。しかしswitch
は(今のところは)極めて限定的なものです。ごく少数の型、numbers, strings, enums、のみswitch可能で、さらに、定数に対する等価性しかテスト出来ません。とはいえこれら制限はおおむね歴史的経緯なだけであり、switch
ステートメントはパターンマッチングにパーフェクトに"マッチ"します*1。もしcase
ラベルでパターンを指定可能になれば、switch
で上述の例を以下のように書けるようになります。
String formatted; switch (obj) { case Integer i: formatted = String.format("int %d", i); break; case Byte b: formatted = String.format("byte %d", b); break; case Long l: formatted = String.format("long %d", l); break; case Double d: formatted = String.format(“double %f", d); break; case String s: formatted = String.format("String %s", s); break default: formatted = obj.toString(); }
これにより、正しく制御構造を使用するようになったため、コードの意図がかなり明瞭になります。つまり、"式obj
は以下の条件のうち少なくとも一つにマッチし、マッチした行を実行する"という意図を示しています。加えて、最適化可能性も良くなり、この例の場合はO(1)でディスパッチ可能となる可能性が高いです。
従来のcase
ラベル(コンパイル時の定数、数値・String
・enum
、との比較)は、両者がObject.equals()
で等価の場合にターゲットが定数パターンにマッチするという点で、定数パターン(constant patterns)と言えます。なお、マッチした定数パターンは何もバインディングしません。
初回の機能拡張では、定数パターン、switch
ステートメントでのバインディングとmatches
式による型テストパターン、のサポートを目的とします。ガード(マッチするにはtrueにならなければならない補助的なboolean式、たとえばcase String s && !s.isEmpty()
と、switch内のcontinue
ステートメント、の両方あるいは片方)をサポートする可能性もあります。
Future Work
型テストパターンとswitchのパターンは最初の一歩に過ぎませんが、それでも明らかに第一歩です。将来的な取り組みの対象となりうる領域(JEPのターゲットになる)には以下があります。
Deconstruction Patterns. クラスには単にデータを保持するものが多くあります。コンストラクタで生成し、その際にN個の引数を取り集約を生成しますが、基本的にはアクセサで一度に一つのコンポーネントをフェッチします。 型テスト-キャスト-バインドの操作と一つの型テストパターンを組み合わせられるので、型テスト-キャスト-複数抽出と一つのdeconstruction patternを組み合わせられます。いま、Node
の型階層があり、そのサブタイプに、IntNode
(単一のintを持つ)、AddNode
とMulNode
(二つのnodeを持つ)、NegNode
(単一のnodeを持つ)があるとすると、Node
に対するマッチをして特定のサブタイプにおける挙動をすべてワンステップに収められます。
int eval(Node n) { switch(n) { case IntNode(int i): return i; case NegNode(Node n): return -eval(n); case AddNode(Node left, Node right): return eval(left) + eval(right); case MulNode(Node left, Node right): return eval(left) * eval(right); default: throw new IllegalStateException(n); }; }
現状、上記のようなアドホックなポリモーフィックな計算を表現するには、"Visitor"パターンを使います。パターンマッチングを使うことで、より透過的で単純な理解しやすいものになります。
Nested Patterns. 上の例で既にnested patternを使用しており、deconstruction patternsの"引数"である、Node n
などは、既にこれ自身がパターン(この場合は型テストパターン)です。いま、左側がゼロのIntNode
のAddNode
とマッチさせたい場合、もう一段階ネストを追加します。
case AddNode(IntNode(0), Node right)
上の例では、deconstruction pattern(AddNode(...)
)で左側のコンポーネントが更に別のeconstruction pattern(IntNode(...)
)にマッチし、このパターンの内側では一つのコンポーネントが定数パターン0
にマッチします。
Expression switch. switch statement
は今のところステートメントですが、いくつかの選択肢から選ぶ場合、結果を生成してそのまま続ける場合が非常に多いです。switch
を式にも出来るようにすることでswitch
ステートメントで式のようなことを実現せざるを得ない歪みを矯正します。
Sealed types. switchのcaseが網羅的であると事前に分かっていることは有用です。つまり、基本的な状況下では決して実行されないdefault
を書く必要が無い、ということです。階層構造に網羅性を組み込めるのであればクライアントに対し有益な制約を示すことになり、コンパイラの網羅性分析の助けになります。
Alternatives
型テストパターン(deconstruction patternsでは無い)はif
とswitch
ステートメントもしくはtype switchのflow typingによっても実現できます。パターンはこれら制御構造を汎用化するものです。
Dependencies
実装にはおそらくDynamic Constants in the JVMを使用します。(https://bugs.openjdk.java.net/browse/JDK-8177279).
*1: perfect "match" for pattern matching.が原文で、意図的にmatchを二回出してると思われ、いわゆるダジャレの匂いがする。