Share
Embed
Java21とKotlinの代数的データ型 & パターンマッチの紹介と本当に嬉しい使い方 / Algebraic Data Type in Java and Kotlin: Happy Use of Pattern Match
JJUG CCC 2023 Fall で発表された内容です。
Java21ではパターンマッチがswitch式で正式に使えるようになります。
https://openjdk.org/jeps/441
これらの変更により型の検査がより強化されて、より実行時例外がすくなくコンパイル時に多くの実装ミスが検出できるようになります。
本セッションではJava21のパターンマッチに関する新機能にふれつつ、先んじて導入しているKotlinとの比較や、実際のプロジェクトでどのように活用するべきかについて話します。
型を使って実際に実装ミスをコンパイルフェーズで発見していくという内容は過去にKotlin Fest 2022で筆者が発表しており、今回はよりJava21の新機能とパターンマッチに着目して解説していく予定です。
https://jjug.doorkeeper.jp/events/164154
YuitoSato
November 11, 2023
Transcript
63 slides
-
2 2 ©2023 Loglass Inc. 自己紹介 株式会社ログラス 開発部 エンジニア 佐藤有斗(X:
@Yuiiitoto) 2020年12月にソフトウェアエンジニアとしてログラスに入社。 KotlinとTypeScriptを使って経営管理クラウドLoglassを開発して いる。 母国語はScala。 KotlinのOSSをちょこちょこ開発・保守しています。 -
4 ©2023 Loglass Inc. Java21のパターンマッチ sealed interface S permits A,
B, C {} final class A implements S {} final class B implements S {} record C(int num, String str) implements S {} static String switchExpression(S s) { return switch (s) { case A a -> "A"; case B b -> "B"; case C(int num, String str) -> Integer.toString(num) + str; // default節がいらない }; } -
5 ©2023 Loglass Inc. Java21のパターンマッチ sealed interface S permits A,
B, C {} final class A implements S {} final class B implements S {} record C(int num, String str) implements S {} static String switchExpression(S s) { return switch (s) { case A a -> "A"; case B b -> "B"; // コンパイルエラー // case C(int num, String str) -> Integer.toString(num) + str; }; } -
7 ©2023 Loglass Inc. 本発表のイメージ ① Java21 Pattern Matching for
switch and Record Patterns -
8 ©2023 Loglass Inc. 本発表のイメージ ② 代数的データ型 ① Java21 Pattern
Matching for switch and Record Patterns より抽象的に -
9 ©2023 Loglass Inc. 本発表のイメージ ② 代数的データ型 ① Java21 Pattern
Matching for switch and Record Patterns ③ 実際の使い所の紹介 より抽象的に より具体的に -
10 ©2023 Loglass Inc. アジェンダ 1. Java21のパターンマッチについて a. JEP441: Pattern
Matching for switch b. JEP440: Record Patterns c. Kotlinとの比較 2. 代数的データ型について a. 直積と直和 b. 代数的データ型の本質 c. 継承と何が違うの? d. Enumと何が違うの? 3. 実際の使い所 a. 直積と直和の性質を使って状態ごとに構造体を切り替える -
11 ©2023 Loglass Inc. アジェンダ 1. Java21のパターンマッチについて a. JEP441: Pattern
Matching for switch b. JEP440: Record Patterns c. Kotlinとの比較 2. 代数的データ型について a. 直積と直和 b. 代数的データ型の本質 c. 継承と何が違うの? d. Enumと何が違うの? 3. 実際の使い所 a. 直積と直和の性質を使って状態ごとに構造体を切り替える -
13 ©2023 Loglass Inc. Java21のパターンマッチ sealed interface S permits A,
B, C {} final class A implements S {} final class B implements S {} record C(int num, String str) implements S {} static String switchExpression(S s) { return switch (s) { case A a -> "A"; case B b -> "B"; case C(int num, String str) -> Integer.toString(num) + str; // default節がいらない }; } -
15 ©2023 Loglass Inc. sealed interface S permits A, B,
C {} final class A implements S {} final class B implements S {} record C(int num, String str) implements S {} static String switchExpression(S s) { return switch (s) { case A a -> "A"; case B b -> "B"; case C(int num, String str) -> Integer.toString(num) + str; // default節がいらない }; } Java21のswitch式のパターンマッチ sealed + permitsで実装 クラスを限定 -
16 ©2023 Loglass Inc. sealed interface S permits A, B,
C {} final class A implements S {} final class B implements S {} record C(int num, String str) implements S {} static String switchExpression(S s) { return switch (s) { case A a -> "A"; case B b -> "B"; case C(int num, String str) -> Integer.toString(num) + str; // default節がいらない }; } Java21のswitch式のパターンマッチ 引数sはAかBかCに 確定するのでdefault節が 必要ない -
17 ©2023 Loglass Inc. Java19まで(switch式内でパターンマッチが使えない) sealed interface S permits A,
B, C {} final class A implements S {} final class B implements S {} record C(int num, String str) implements S {} static String switchExpression(S s) { return switch (s) { case A a -> "A"; case B b -> "B"; case C(int num) -> Integer.toString(num) + str; }; } // => error: patterns in switch statements are a preview feature and are disabled by default. switch式内で 型判定ができない -
18 ©2023 Loglass Inc. Java19まで static String ifStatementPatternMatch(S s) {
if (s instanceof A a) { return "A"; } else if (s instanceof B b) { return "B"; } else if (s instanceof C c) { return Integer.toString(c.num()) + c.str(); } else { return null; } } - if節で頑張って書く -
19 ©2023 Loglass Inc. Java19まで static String ifStatementPatternMatch(S s) {
if (s instanceof A a) { return "A"; } else if (s instanceof B b) { return "B"; } else if (s instanceof C c) { return Integer.toString(c.num()) + c.str(); } else { return null; } } else節を省略できない - if節で頑張って書く -
21 ©2023 Loglass Inc. sealed interface S permits A, B,
C {} final class A implements S {} final class B implements S {} record C(int num, String str) implements S {} static String switchExpression(S s) { return switch (s) { case A a -> "A"; case B b -> "B"; case C(int num, String str) -> Integer.toString(num) + str; }; } Java21でRecordがパターンマッチで使えるようになった Cクラスのフィールドに 手軽にアクセスできる -
23 ©2023 Loglass Inc. Kotlinとの比較 sealed interface S class A
: S class B : S data class C(val num: Int, val str: String) : S fun switchExpression(s: S): String { return when (s) { is A -> "A" is B -> "B" is C -> s.num.toString() + s.str // else節(Javaでいうdefault節)がいらない } } -
24 ©2023 Loglass Inc. Kotlinとの比較 sealed interface S class A
: S class B : S data class C(val num: Int, val str: String) : S fun switchExpression(s: S): String { return when (s) { is A -> "A" is B -> "B" is C -> s.num.toString() + s.str // else節(Javaでいうdefault節)がいらない } } sealed で実装クラスを限定 Javaのsealed ~ permits相当 -
25 ©2023 Loglass Inc. Kotlinとの比較 sealed interface S class A
: S class B : S data class C(val num: Int, val str: String) : S fun switchExpression(s: S): String { return when (s) { is A -> "A" is B -> "B" is C -> s.num.toString() + s.str // else節(Javaでいうdefault節)がいらない } } 引数sはAかBかCに 確定するのでelse節が 必要ない -
27 ©2023 Loglass Inc. JavaとKotlinの違い①: Kotlinは個別のフィールドのパターンマッチができない static String switchExpression(S s)
{ return switch (s) { case A a -> "A"; case B b -> "B"; case C(int num, String str) -> Integer.toString(num) + str; }; } fun switchExpression(s: S): String { return when (s) { is A -> "A" is B -> "B" is C -> s.num.toString() + s.str } } -
29 ©2023 Loglass Inc. KotlinのNull Safety fun switchExpression(s: S): String
{ return when (s) { is A -> "A" is B -> "B" is C -> s.num.toString() + s.str // null -> "null" } } fun switchExpression2(s: S?): String { return when (s) { is A -> "A" is B -> "B" is C -> s.num.toString() + s.str null -> "null" } } ?がついていない限りは Nullをコンパイルフェーズで 防げる。 ?がついている場合は nullのケースが必要 (でないとコンパイルエラーになる) -
30 ©2023 Loglass Inc. Javaのswitch式内のnullの扱い sealed interface S permits A,
B, C {} final class A implements S {} final class B implements S {} record C(int num, String str) implements S {} static String switchExpression(S s) { return switch (s) { case A a -> "A"; case B b -> "B"; case C(int num, String str) -> Integer.toString(num) + str; null -> "null" }; } switch式の中でnullが扱えるようになった。 しかし常にnullに気を配る必要がある。 nullがなくてもコンパイルエラーにならない。 -
32 ©2023 Loglass Inc. アジェンダ 1. Java21のパターンマッチについて a. JEP441: Pattern
Matching for switch b. JEP440: Record Patterns c. Kotlinとの比較 2. 代数的データ型について a. 直積と直和 b. 代数的データ型の本質 c. 継承と何が違うの? d. Enumと何が違うの? 3. 実際の使い所 a. 直積と直和の性質を使って状態ごとに構造体を切り替える -
34 ©2023 Loglass Inc. 理論を学ぼう ② 代数的データ型 ① Java21 Pattern
Matching for switch and Record Patterns ③ 実際の使い所の紹介 より抽象的に より具体的に -
35 ©2023 Loglass Inc. 代数的データ型(ADT, Algebraic Data Type)とは? - 代数学に由来するデータ構造
- 直積集合と直和集合で表されるデータ構造 -
36 ©2023 Loglass Inc. 直積集合について - A × B =
{ (a,b) ∣ a ∈ A, b ∈ B } (=AかつB) - JavaではRecordが直積に当たる(Classも直積) - 要するに構造体 (1, “あ”) (1, “い”) (1, “う”) (2, “あ”) (2, “い”) (2, “う”) (3, “あ”) (3, “い”) (3, “う”) A: 1, 2, 3, B: “あ”, “い”, ‘う“ A ✖ B record C ( int num, String str ) implements S {} C = int ✖ String -
37 ©2023 Loglass Inc. 直和集合について - A ⊕ B= A
∪ B ただし A ∩ B = {0} (=AまたはBだけどAとBは被らない) - Javaではsealed ~ permits ~ が直和にあたる A B A ⊕ B sealed interface S permits A, B, C {} S = A ⊕ B ⊕ C -
38 ©2023 Loglass Inc. 代数的データ型 = 直積集合と直和集合の掛け合わせ A B C
= int ✖ String S = A ⊕ B ⊕ (int ✖ String) sealed interface S permits A, B, C {} final class A implements S {} final class B implements S {} record C( int num, String str ) implements S {} -
40 ©2023 Loglass Inc. 継承とは何が違うの? A B C = int
✖ String - 継承は範囲を閉じることができない(親クラスが子クラスを知らない) - 直和の性質がない D = boolean ✖ boolean S 知らないところで 継承してるかも? -
41 ©2023 Loglass Inc. Enumと何が違うの? A B C = int
✖ String - Enumは個別の構造体が持てない - 直積の性質がない S -
44 ©2023 Loglass Inc. 使い所を学ぼう ② 代数的データ型 ① Java21 Pattern
Matching for switch and Record Patterns ③ 実際の使い所の紹介 より抽象的に より具体的に -
45 ©2023 Loglass Inc. アジェンダ 1. Java21のパターンマッチについて a. JEP441: Pattern
Matching for switch b. JEP440: Record Patterns c. Kotlinとの比較 2. 代数的データ型について a. 直積と直和 b. 代数的データ型の本質 c. 継承と何が違うの? d. Enumと何が違うの? 3. 実際の使い所 a. 直積と直和の性質を使って状態ごとに構造体を切り替える -
46 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - タスク管理アプリ(お決まりのやつ)を考える - Taskクラスの状態はInProgress→Completed -
Completedの場合は必ずcompletedAtを持つ class Task { private final String title; private final TaskStatus status; private final LocalDateTime completedAt; // 中略 } enum TaskStatus { InProgress, Completed } 従来の書き方 InProgress Completed -
47 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - データ不整合がおきる可能性がある - 例) 完了日時を持つ進行中のタスク
InProgress Completed 進行中なのに完了時刻 を持っている? new Task("買い物", TaskStatus.InProgress, null); new Task( "買い物", TaskStatus.Completed, LocalDateTime.now() ); new Task( "買い物", TaskStatus.InProgress, LocalDateTime.now() ); // NG: 進行中なのに完了時刻持ち 従来の書き方 -
49 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - Task全体を代数的データ型と捉える sealed interface Task
permits InProgressTask, CompletedTask { String title(); } record InProgressTask( String title ) implements Task {} record CompletedTask( String title, LocalDateTime completedAt ) implements Task {} InProgressTask = String Task = InProgressTask ⊕ CompletedTask CompletedTask = String × LocalDateTime -
50 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - Enumとは違いデータ不整合がコンパイルフェーズで検出される InProgressTask = String
Task = InProgressTask ⊕ CompletedTask CompletedTask = String × LocalDateTime new InProgressTask("買い物"); new CompletedTask( "買い物", LocalDateTime.now() ); // コンパイルエラー new InProgressTask( "買い物", LocalDateTime.now() ); -
51 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - Enumとは違いデータ不整合がコンパイルフェーズで検出される - どちらか2つの集合の中で値が必ず確定する InProgressTask
= String Task = InProgressTask ⊕ CompletedTask CompletedTask = String × LocalDateTime new InProgressTask("買い物"); new CompletedTask( "買い物", LocalDateTime.now() ); // コンパイルエラー new InProgressTask( "買い物", LocalDateTime.now() ); -
52 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - Enumとは違いデータ不整合がコンパイルフェーズで検出される - どちらか2つの集合の中で値が必ず確定する。 InProgressTask
= String InProgressTask ∩ CompletedTask = { 0 } CompletedTask = String × LocalDateTime new InProgressTask("買い物"); new CompletedTask( "買い物", LocalDateTime.now() ); // コンパイルエラー new InProgressTask( "買い物", LocalDateTime.now() ); 進行中なのに完了時刻 を持っている? -
53 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - Enumのようにswitch式も使える sealed interface Task
permits InProgressTask, CompletedTask { String title(); default Task complete(LocalDateTime completedAt) { return switch (this) { case InProgressTask t -> new CompletedTask( t.title(), completedAt ); case CompletedTask t -> t; }; } } -
54 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - 新しいステータスが増えた場合もコンパイルエラーで変更箇所を自動検知可能 InProgressTask = String
CompletedTask = String × LocalDateTime InReviewTask = String sealed interface Task permits InProgressTask, CompletedTask, InReviewTask { String title(); } record InProgressTask( String title ) implements Task {} record CompletedTask( String title, LocalDateTime completedAt ) implements Task {} ++ record InReviewTask( ++ String title ++ ) implements Task { } -
55 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - 新しいステータスが増えた場合もコンパイルエラーで変更箇所を自動検知可能 InProgressTask = String
CompletedTask = String × LocalDateTime InReviewTask = String sealed interface Task permits InProgressTask, CompletedTask, InReviewTask { String title(); } record InProgressTask( String title ) implements Task {} record CompletedTask( String title, LocalDateTime completedAt ) implements Task {} ++ record InReviewTask( ++ String title ++ ) implements Task { } -
56 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - 新しいステータスが増えた場合もコンパイルエラーで変更箇所を自動検知可能 - パターン網羅性の自動担保 sealed
interface Task permits InProgressTask, CompletedTask { String title(); default Task complete(LocalDateTime completedAt) { return switch (this) { case InProgressTask t -> new CompletedTask( t.title(), completedAt ); case CompletedTask t -> t; // InReviewTaskがないのでコンパイルエラー }; } } InProgressTask = String CompletedTask = String × LocalDateTime InReviewTask = String -
57 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - 改めて比較してみる class Task {
private final String title; private final TaskStatus status; private final LocalDateTime completedAt; // 中略 } enum TaskStatus { InProgress, Completed } sealed interface Task permits InProgressTask, CompletedTask { String title(); } record InProgressTask( String title ) implements Task {} record CompletedTask( String title, LocalDateTime completedAt ) implements Task {} 代数的データ型 従来の書き方 -
58 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - 改めて比較してみる - コンパイルフェーズでデータ不整合を防げている new
InProgressTask("買い物"); new CompletedTask( "買い物", LocalDateTime.now() ); // コンパイルエラー new InProgressTask( "買い物", LocalDateTime.now() ); 代数的データ型 従来の書き方 new Task("買い物", TaskStatus.InProgress, null); new Task( "買い物", TaskStatus.Completed, LocalDateTime.now(), ); // NG: 進行中なのに完了時刻持ち new Task( "買い物", TaskStatus.InProgress, LocalDateTime.now() );