Upgrade to Pro — share decks privately, control downloads, hide ads and more …

動くコードを書こう / let's code a process

動くコードを書こう / let's code a process

2023/11/11に開催されたJJUG CCC 2023 Fallでの登壇資料です

Naoki Kishida

November 11, 2023
Tweet

More Decks by Naoki Kishida

Other Decks in Programming

Transcript

  1. 動くプログラムを書こう
    2023-11-11 JJUG CCC 2023 Fall
    LINEヤフーコミュニケーションズ
    きしだ なおき(@kis)
    逐次実行と状態遷移を理解して処理を書けるようになろう

    View full-size slide

  2. 2023/11/11 2
    自己紹介

    きしだ なおき

    LINEヤフーコミュニケーションズ
    (旧LINE Fukuoka)

    twitter: @kis

    blog: きしだのHatena

    (nowokay.hatenablog.com)

    「プロになるJava」というJava入門書を書いてます

    View full-size slide

  3. 2023/11/11 3
    わかりますか?
    x = x + 1

    View full-size slide

  4. わかりますか?

    x = x + 1

    イディオムとして形で理解してる人もいるかも

    View full-size slide

  5. わかりますか?

    x = (x = x + 1) + x

    View full-size slide

  6. わかりますか?

    x = (x = x + 1) + x

    ここではどのように追えばいいかわかればOK

    View full-size slide

  7. わからないときの差は?
    ● x = (x = x + 1) + x では「=」を式として使っている

    しかしそれを知っただけでわかるわけではない

    ではなぜそこでわからなくなる?

    var y = x = 1は「=」を式として使っているけど理解しやすい

    View full-size slide

  8. x = x + 1の難しさ

    プログラムが通常の式や文章と違うことが表されている

    演算、逐次実行、状態遷移が含まれている

    プログラムの処理の本質

    計算機も論理回路、クロック、フリップフロップでできている

    x := x + 1や x + 1 → xのように表記を変えても解決になりにくい
    x = x + 1
    演算
    状態遷移
    状態遷移
    逐次実行

    View full-size slide

  9. 逐次実行をちゃんとやろう

    x = x + 1がわからないのは逐次実行がわかっていないから

    演算は算数でのトレーニングが活かせる

    ここでの状態遷移は複雑ではない

    変数、ループがわからないのは逐次実行がわかっていない

    そこまでごまかせていたものが表出するだけ

    x = (x = x + 1) + x がわからないのも

    View full-size slide

  10. ループ

    まずは、各回が独立したループを考える

    今日の話は、最初は簡単と思っていても
    どこかからなぜか難しくなります

    View full-size slide

  11. 同じ処理を無限に繰り返す

    同じことを無限に繰り返すのはループの中で一番簡単
    void main() {
    for (;;) {
    System.out.println("Hello");
    }
    }
    ※ Java 21ではクラスやメインメソッド引数が省略できるようになっています
    ※ --enable-previewが必要

    View full-size slide

  12. 回数が決まっている

    回数が決まったループは少し面倒
    void main() {
    for (int i = 0; i < 5; i++) {
    System.out.println("Hello");
    }
    }

    View full-size slide

  13. rangeを使って回数指定

    IntStream.rangeを使うとわかりやすい

    けど構文がちょっと複雑
    import static java.util.stream.IntStream.range;
    void main() {
    range(0, 5).forEach(_ -> {
    System.out.println("Hello")
    });
    }

    View full-size slide

  14. ループ変数の値を使う

    毎回やることが少し変わる
    void main() {
    for (int i = 0; i < 5; i++) {
    System.out.println(STR."Hello \{i}");
    }
    }
    ※ Java 21では文字列に式が埋め込めるようになっています
    ※ --enable-previewが必要

    View full-size slide

  15. リストを使ったループ

    拡張forでリストを使うループ
    import java.util.List;
    void main() {
    for (var s : List.of("apple", "banana")) {
    System.out.println(s.toUpperCase());
    }
    }

    View full-size slide

  16. 値の集約

    リストをひとつの値に集約する

    View full-size slide

  17. ループによる値の集約

    ループを使って合計
    void main() {
    int[] data ={23, 76, 43, 9, 17, 32, 59};
    var result = 0;
    for (int n : data) {
    result += n;
    }
    System.out.println(result);
    }

    View full-size slide

  18. 条件を全てが満たすかどうか

    ループで実装
    void main() {
    int[] data ={23, 76, 43, 9, 17, 32, 59};
    var result = true;
    for (int n : data) {
    result &= n % 2 == 1;
    }
    }

    View full-size slide

  19. 条件を満たす値のリスト

    ループで実装

    数値配列を動的に構築するときはIntStream.builder
    import java.util.stream.IntStream;
    void main() {
    int[] data ={23, 76, 43, 9, 17, 32, 59};
    var result = IntStream.builder();
    for (int n : data) {
    if (n % 2 == 1) continue;
    result.add(n);
    }
    var nums = result.build().toArray();
    }

    View full-size slide

  20. 集約の基本構造

    共通の構造がある

    仕組み化できる

    Stream var result = 初期値
    for (繰り返し) {
    処理
    resultへの追加処理
    }

    View full-size slide

  21. Streamによる値の集約

    streamによる合計
    import java.util.stream.IntStream;
    void main() {
    int[] data ={23, 76, 43, 9, 17, 32, 59};
    int result = IntStream.of(data)
    .sum();
    }

    View full-size slide

  22. 条件を全てが満たすかどうか

    streamによる判定
    import java.util.stream.IntStream;
    void main() {
    int[] data ={23, 76, 43, 9, 17, 32, 59};
    var result = IntStream.of(data)
    .allMatch(n -> n % 2 == 1);
    System.out.println(result);
    }

    View full-size slide

  23. 条件を満たす値のリスト

    streamによるリスト生成
    import java.util.stream.IntStream;
    void main() {
    int[] data ={23, 76, 43, 9, 17, 32, 59};
    var result = IntStream.of(data)
    .filter(n -> n % 2 == 1)
    .toArray();
    System.out.println(result);
    }

    View full-size slide

  24. 終端処理と演算

    まとめると、こう
    終端処理 初期値 更新処理
    sum 0 +
    allMatch true &
    anyMatch false |
    toList new ArrayList() add

    View full-size slide

  25. 単位元

    初期値は単位元になっている

    単位元

    a = e a = a e
    ★ ★ となる値a

    足し算の場合0

    掛け算の場合1

    リストの結合の場合、空リスト
    終端処理 初期値 更新処理
    sum 0 +
    allMatch true &
    anyMatch false |
    toList new ArrayList() add

    View full-size slide

  26. 前後のデータに依存するループ

    Streamで書けない

    ただしJava 22でGathererによって可能になるかも

    View full-size slide

  27. 移動平均

    他のデータを参照する

    複数のデータで平均をとる

    平均をとる範囲が移動していく

    なめらかにする

    ローカットフィルタ

    例: 日次売上を7日で移動平均

    曜日の影響を排除できる
    import java.util.Arrays;
    import java.util.stream.IntStream;
    void main() {
    int[] data ={23, 76, 43, 9, 17, 32, 59};
    var builder = IntStream.builder();
    for (int i = 0; i < data.length - 2; i++) {
    int sum = Arrays.stream(data, i, i + 3).sum();
    builder.add(sum / 3);
    }
    var result = builder.build().toArray();
    System.out.println(Arrays.toString(result));
    }

    View full-size slide

  28. 移動平均の計算量

    二重ループがある

    O(nm)

    n = データ数

    m=ウィンドウサイズ

    View full-size slide

  29. 二重ループのない移動平均

    二重ループを排除できる

    O(n)

    ウィンドウサイズに依存しない

    スライディングウィンドウ

    入ってくるデータと
    出ていくデータを
    かんがえる
    import java.util.ArrayList;
    import java.util.stream.IntStream;
    void main() {
    int[] data ={23, 76, 43, 9, 17, 32, 59};
    var builder = IntStream.builder();
    var window = new ArrayList();
    var sum = 0;
    for (var n : data) {
    sum += n;
    window.addLast(n);
    while (window.size() > 3) {
    sum -= window.removeFirst();
    }
    if (window.size() == 3) builder.add(sum /3);
    }
    }

    View full-size slide

  30. 隠れた状態を利用する

    データ直接ではないとき難しい

    View full-size slide

  31. カッコの対応を判定する

    カッコの対応を判定

    “abc(def)ghi” ->
    🙆

    “abc((def)ghi” ->
    🙅‍♀️

    View full-size slide

  32. カッコの対応を判定する処理

    カウンタを使う

    View full-size slide

  33. 隠れた状態さえ見つければ実装は簡単

    割と素直

    コードから難しさが
    わかりにくい
    boolean check(String data) {
    int count = 0;
    for (var ch : data.toCharArray()) {
    switch (ch) {
    case '(' -> count++;
    case ')' -> {
    count--;
    if (count < 0) {
    return false;
    }
    }
    }
    }
    return count == 0;
    }

    View full-size slide

  34. 複雑な状態遷移

    状態が入り組むことがある

    状態遷移としてまとめる

    View full-size slide

  35. 状態遷移図

    状態遷移を表す図

    例: 10進数整数

    View full-size slide

  36. 状態遷移のコード

    状態遷移図の実装 boolean check(String s) {
    enum State { START, ZERO, INT }
    var state = State.START;
    for (char ch : s.toCharArray()) {
    switch (state) {
    case START -> {
    if (ch == '0')
    state = State.ZERO;
    else if (ch >= '1' && ch <= '9')
    state = State.INT;
    else return false;
    }
    case ZERO -> {
    return false;
    }
    case INT -> {
    if (ch < '0' || ch > '9') return false;
    }
    }
    }
    return state != State.START;
    }

    View full-size slide

  37. 状態遷移はいろいろ現れる

    リクエストを受けるごとに状態が遷移

    例: 購入->入金->配送

    場当たり的にフラグ管理をして破綻しがち

    View full-size slide

  38. 正規表現と正規言語

    正規表現であらわせる
    boolean check(String s) {
    return s.matches("0|[1-9][0-9]*");
    }

    View full-size slide

  39. 逐次実行と状態遷移

    逐次実行はプログラムカウンタの状態遷移
    void main() {
    System.out.println("Hello");
    System.out.println("World");
    }

    View full-size slide

  40. クロックで実行処理を決める

    プログラムカウンタを使う
    int counter = 0;
    void clock() {
    counter = (counter + 1) % 2;
    switch (counter) {
    case 0 -> System.out.println("hello");
    case 1 -> System.out.println("world");
    }
    }
    void main() {
    for (;;) clock();
    }

    View full-size slide

  41. 命令で処理を共通化

    ノイマン型アーキテクチャ import java.util.List;
    sealed interface Op permits Output, Goto {} // 命令
    record Output(String message) implements Op {}
    record Goto(int no) implements Op {}
    List codes = List.of( // プログラム
    new Output("hello"),
    new Output("world"),
    new Goto(0));
    int counter = 0;
    void clock() { // 命令実行
    counter++;
    switch(codes.get(counter-1)) { // 命令ごとの分岐
    case Output(var msg) -> System.out.println(msg);
    case Goto(var no) -> counter = no;
    }
    }
    void main(String[] args) {
    for (;;) clock();
    }

    View full-size slide

  42. 再帰

    メソッド内で自分自身を呼び出す

    View full-size slide

  43. 再帰によるループ

    再帰はループになる
    void main() {
    System.out.println("hello");
    main();
    }

    View full-size slide

  44. 回数の決まった再帰

    条件をいれて止める
    void loop(int i) {
    if (i -> 5) {
    return;
    }
    System.out.println(STR."Hello \{i}");
    loop(i + 1);
    }
    void main() {
    loop(0);
    }

    View full-size slide

  45. 回数の決まった再帰のパターン

    パターンを覚えれば書きやすい
    戻り値 処理(引数) {
    if (終了条件) {
    return 再帰を行わない値
    }
    return 処理(加工した引数)
    }

    View full-size slide

  46. 入れ子になった状態遷移

    複数のカッコの対応を判定

    再帰を使うと書きやすい

    スタックの隠蔽
    void check(String str) {
    idx = 0;
    System.out.println(STR."\{str} \{check(str, 0)}");
    }
    int idx;
    boolean check(String str, int paren) {
    for (; idx < str.length(); idx++) {
    char ch = str.charAt(idx);
    switch (ch) {
    case '(', '{', '[' -> {
    idx++;
    if (!check(str,ch)) return false;
    }
    case ')', '}', ']' -> {
    return ch == paren + (paren == '(' ? 1 : 2);
    }
    }
    }
    return paren == 0;
    }

    View full-size slide

  47. 文脈自由言語

    入れ子のある正規表現

    構文解析に使える

    View full-size slide

  48. 初期化のある再帰のパターン

    初期化がある場合
    戻り値 処理(引数) {
    初期化
    処理impl(引数, 追加のデータ)
    }
    戻り値 処理impl(引数, 追加のデータ) {
    if (終了条件) {
    return 再帰を行わない値
    }
    return 処理impl(加工した引数, 加工した追加のデータ)
    }

    View full-size slide

  49. 深さの変わるループ

    多重ループ

    n重ループを実装するには?
    import static java.util.FormatProcessor.FMT;
    void main() {
    for (int i = 1; i <= 9; i++) {
    for (int j = 1; j <= 9; j++) {
    System.out.print(FMT."\{i}*\{j}=%2d\{i*j} ");
    }
    System.out.println();
    }
    }

    View full-size slide

  50. 再帰による可変ループ

    再帰によって可変にする
    void loop(int n) {
    loopImpl(n, 0);
    }
    void loopImpl(int n, int c) {
    if (n == c) {
    System.out.println("なにか処理");
    return;
    }
    for (int i = 0; i < n; ++i) {
    loopImpl(n, c + 1);
    }
    }

    View full-size slide

  51. もっと勉強するには

    プロになるJava

    WEB+DB PRESS Vol.135, Vol.136

    数学ガール 乱択アルゴリズム

    View full-size slide

  52. AI時代にも価値を出せるように

    ライブラリの使い方は「AI」がわかる

    単純な知識の蓄積に意味がなくなる

    検索で暗記の必要はなくなっていた

    手に覚えさせることも不要になりつつある

    プログラムの処理が発想できること把握できることが大切

    正しく表現できる、読み取れることが大切

    発想したあとのコードは「AI」が書いてくれる

    View full-size slide