$30 off During Our Annual Pro Sale. View Details »

Java21とKotlinの代数的データ型 & パターンマッチの紹介と本当に嬉しい使い方 / Algebraic Data Type in Java and Kotlin: Happy Use of Pattern Match

YuitoSato
November 11, 2023

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
Tweet

More Decks by YuitoSato

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  22. 22
    ©2023 Loglass Inc.
    Kotlinとの比較

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  26. 26
    ©2023 Loglass Inc.
    ほぼ同じ  

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  36. 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

    View Slide

  37. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  46. 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

    View Slide

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

    View Slide

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

    View Slide

  49. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  56. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  63. 63

    View Slide