Slide 1

Slide 1 text

1 ©2023 Loglass Inc. Java21とKotlinの代数的データ型 & パターンマッチの紹介と本当に嬉しい使い方 2023.11.11 佐藤有斗(@Yuiiitoto) 株式会社ログラス

Slide 2

Slide 2 text

2 2 ©2023 Loglass Inc. 自己紹介 株式会社ログラス 開発部 エンジニア 佐藤有斗(X: @Yuiiitoto) 2020年12月にソフトウェアエンジニアとしてログラスに入社。 KotlinとTypeScriptを使って経営管理クラウドLoglassを開発して いる。 母国語はScala。 KotlinのOSSをちょこちょこ開発・保守しています。

Slide 3

Slide 3 text

3 ©2023 Loglass Inc. Java21でswitch(式/文)のパターンマッチが 使えるようになりました! 🎉 🎊 🎊 🎉 🎉 🎊 🎊 🎊 🎉

Slide 4

Slide 4 text

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節がいらない }; }

Slide 5

Slide 5 text

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; }; }

Slide 6

Slide 6 text

6 ©2023 Loglass Inc. 本発表のメインメッセージ パターンマッチの裏にある 代数的データ型という理論と その嬉しい使い方を理解してほしい

Slide 7

Slide 7 text

7 ©2023 Loglass Inc. 本発表のイメージ ① Java21 Pattern Matching for switch and Record Patterns

Slide 8

Slide 8 text

8 ©2023 Loglass Inc. 本発表のイメージ ② 代数的データ型 ① Java21 Pattern Matching for switch and Record Patterns より抽象的に

Slide 9

Slide 9 text

9 ©2023 Loglass Inc. 本発表のイメージ ② 代数的データ型 ① Java21 Pattern Matching for switch and Record Patterns ③ 実際の使い所の紹介 より抽象的に より具体的に

Slide 10

Slide 10 text

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. 直積と直和の性質を使って状態ごとに構造体を切り替える

Slide 11

Slide 11 text

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. 直積と直和の性質を使って状態ごとに構造体を切り替える

Slide 12

Slide 12 text

12 ©2023 Loglass Inc. 1: Java21のパターンマッチについて

Slide 13

Slide 13 text

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節がいらない }; }

Slide 14

Slide 14 text

14 ©2023 Loglass Inc. https://openjdk.org/jeps/441

Slide 15

Slide 15 text

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で実装 クラスを限定

Slide 16

Slide 16 text

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節が 必要ない

Slide 17

Slide 17 text

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式内で 型判定ができない

Slide 18

Slide 18 text

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節で頑張って書く

Slide 19

Slide 19 text

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節で頑張って書く

Slide 20

Slide 20 text

20 ©2023 Loglass Inc. https://openjdk.org/jeps/440

Slide 21

Slide 21 text

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クラスのフィールドに 手軽にアクセスできる

Slide 22

Slide 22 text

22 ©2023 Loglass Inc. Kotlinとの比較

Slide 23

Slide 23 text

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節)がいらない } }

Slide 24

Slide 24 text

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相当

Slide 25

Slide 25 text

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節が 必要ない

Slide 26

Slide 26 text

26 ©2023 Loglass Inc. ほぼ同じ  

Slide 27

Slide 27 text

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 } }

Slide 28

Slide 28 text

28 ©2023 Loglass Inc. JavaとKotlinの違い②: Nullの扱い switchExpression(null) => 実行時エラー switchExpression(null) => コンパイルエラー

Slide 29

Slide 29 text

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のケースが必要 (でないとコンパイルエラーになる)

Slide 30

Slide 30 text

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がなくてもコンパイルエラーにならない。

Slide 31

Slide 31 text

31 ©2023 Loglass Inc. まとめ: Nullの扱いにおいてはKotlinに1歩劣るが Javaの方が複雑なパターンマッチができる

Slide 32

Slide 32 text

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. 直積と直和の性質を使って状態ごとに構造体を切り替える

Slide 33

Slide 33 text

33 ©2023 Loglass Inc. 2: 代数的データ型について

Slide 34

Slide 34 text

34 ©2023 Loglass Inc. 理論を学ぼう ② 代数的データ型 ① Java21 Pattern Matching for switch and Record Patterns ③ 実際の使い所の紹介 より抽象的に より具体的に

Slide 35

Slide 35 text

35 ©2023 Loglass Inc. 代数的データ型(ADT, Algebraic Data Type)とは? - 代数学に由来するデータ構造 - 直積集合と直和集合で表されるデータ構造

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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 {}

Slide 39

Slide 39 text

39 ©2023 Loglass Inc. 代数的データ型 = 直積集合と直和集合の掛け合わせ

Slide 40

Slide 40 text

40 ©2023 Loglass Inc. 継承とは何が違うの? A B C = int ✖ String - 継承は範囲を閉じることができない(親クラスが子クラスを知らない) - 直和の性質がない D = boolean ✖ boolean S 知らないところで 継承してるかも?

Slide 41

Slide 41 text

41 ©2023 Loglass Inc. Enumと何が違うの? A B C = int ✖ String - Enumは個別の構造体が持てない - 直積の性質がない S

Slide 42

Slide 42 text

42 ©2023 Loglass Inc. で、何が嬉しいの?

Slide 43

Slide 43 text

43 ©2023 Loglass Inc. 代数的データ型の嬉しい所: 取りうるデータの範囲を型で表現できる

Slide 44

Slide 44 text

44 ©2023 Loglass Inc. 使い所を学ぼう ② 代数的データ型 ① Java21 Pattern Matching for switch and Record Patterns ③ 実際の使い所の紹介 より抽象的に より具体的に

Slide 45

Slide 45 text

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. 直積と直和の性質を使って状態ごとに構造体を切り替える

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

47 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - データ不整合がおきる可能性がある - 例) 完了日時を持つ進行中のタスク InProgress Completed 進行中なのに完了時刻 を持っている? new Task("買い物", TaskStatus.InProgress, null); new Task( "買い物", TaskStatus.Completed, LocalDateTime.now() ); new Task( "買い物", TaskStatus.InProgress, LocalDateTime.now() ); // NG: 進行中なのに完了時刻持ち 従来の書き方

Slide 48

Slide 48 text

48 ©2023 Loglass Inc. 代数的データ型を使おう

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

50 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - Enumとは違いデータ不整合がコンパイルフェーズで検出される InProgressTask = String Task = InProgressTask ⊕ CompletedTask CompletedTask = String × LocalDateTime new InProgressTask("買い物"); new CompletedTask( "買い物", LocalDateTime.now() ); // コンパイルエラー new InProgressTask( "買い物", LocalDateTime.now() );

Slide 51

Slide 51 text

51 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - Enumとは違いデータ不整合がコンパイルフェーズで検出される - どちらか2つの集合の中で値が必ず確定する InProgressTask = String Task = InProgressTask ⊕ CompletedTask CompletedTask = String × LocalDateTime new InProgressTask("買い物"); new CompletedTask( "買い物", LocalDateTime.now() ); // コンパイルエラー new InProgressTask( "買い物", LocalDateTime.now() );

Slide 52

Slide 52 text

52 ©2023 Loglass Inc. 直積と直和の性質を使って状態ごとに構造体を切り替える - Enumとは違いデータ不整合がコンパイルフェーズで検出される - どちらか2つの集合の中で値が必ず確定する。 InProgressTask = String InProgressTask ∩ CompletedTask = { 0 } CompletedTask = String × LocalDateTime new InProgressTask("買い物"); new CompletedTask( "買い物", LocalDateTime.now() ); // コンパイルエラー new InProgressTask( "買い物", LocalDateTime.now() ); 進行中なのに完了時刻 を持っている?

Slide 53

Slide 53 text

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; }; } }

Slide 54

Slide 54 text

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 { }

Slide 55

Slide 55 text

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 { }

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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 {} 代数的データ型 従来の書き方

Slide 58

Slide 58 text

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() );

Slide 59

Slide 59 text

59 ©2023 Loglass Inc. 代数的データ型の嬉しい所: 取りうるデータの範囲を型で表現できる

Slide 60

Slide 60 text

60 ©2023 Loglass Inc. 代数的データ型の嬉しい所: 取りうるデータの範囲を型で表現できる => コンパイルフェーズで品質を保証する

Slide 61

Slide 61 text

61 ©2023 Loglass Inc. Java21で良い代数的データ型ライフを🎉

Slide 62

Slide 62 text

62 ©2023 Loglass Inc. ありがとうございました!質問は X でも受け付けます!直接の質問も大歓迎 󰢐 https://twitter.com/Yuiiitoto

Slide 63

Slide 63 text

63