Slide 1

Slide 1 text

AI時代を乗り切るための実装力をつけよう 2024-02-15 Developer Summit 2024 LINEヤフー きしだ なおき(@kis)

Slide 2

Slide 2 text

2024/02/15 2 自己紹介 ● きしだ なおき ● LINEヤフー ● X(twitter): @kis ● blog: きしだのHatena ● (nowokay.hatenablog.com) ● 「プロになるJava」というJavaの本を書いてます

Slide 3

Slide 3 text

2024/02/15 3 プロになるJava ● Javaを使ったプログラミング入門書 ● 単にJavaの文法解説書にならないように工夫 ● 多くの入門書は単に文法解説になっている ● 文法を「覚えた」からといってプログラムが書ける わけではない ● 入門書を読んだけどプログラムが書けない問題 ● プログラムを書くには「プログラミング」という 能力が必要 ● なるべく慣れや経験だけに頼らないで再現可能にする必要もある

Slide 4

Slide 4 text

2024/02/15 4 AI時代にプログラマは必要か? ● コードはChatGPTが書いてくれる ● プログラミングを知らなくてもいいのでは? ● 実際はプログラミングを知らない人が生成したコードがプロダクトに 入ることが問題になりつつある ● レビューで指摘しても修正できない ● そもそもレビューの意味が伝わらない ● レビューができない

Slide 5

Slide 5 text

AI時代でもプログラマは必要 ● プログラムの外とのつながりはプログラマが設定する必要がある ● AIに投入する情報は人間が与えないといけない ● 特定の言語やライブラリ、フレームワークの使い方に詳しいことの価 値は薄れている ● 処理がちゃんと書けることが大切 ● 今回は状態遷移に注目

Slide 6

Slide 6 text

2024/02/15 6 わかりますか? x = x + 1

Slide 7

Slide 7 text

わかりますか? ● x = x + 1 ● イディオムとして形で理解してる人もいるかも

Slide 8

Slide 8 text

わかりますか? ● x = (x = x + 1) + x

Slide 9

Slide 9 text

わかりますか? ● x = (x = x + 1) + x ● 「=」は変数に値を割り当てると共に、割り当てた値を返す演算子 ● ここではどのように追えばいいかわかればOK

Slide 10

Slide 10 text

わからないときの差は? ● x = (x = x + 1) + x では「=」を式として使っている ● しかしそれを知っただけでわかるわけではない ● ではなぜそこでわからなくなる? ● var y = x = 1は「=」を式として使っているけど理解しやすい

Slide 11

Slide 11 text

x = x + 1の難しさ ● プログラムには通常の式や文章と違うことが表されている ● 演算、逐次実行、状態遷移が含まれている ● プログラムの処理の本質 ● 計算機も論理回路、クロック、フリップフロップでできている ● x := x + 1や x + 1 → xのように表記を変えても解決になりにくい x = x + 1 演算 状態遷移 状態遷移 逐次実行

Slide 12

Slide 12 text

逐次実行をちゃんとやろう ● 逐次実行はプログラムに特有 ● 演算は算数でのトレーニングが活かせる ● ここでの状態遷移は複雑ではない ● 変数自体はわかる人が多い ● 変数、ループがわからないのは逐次実行がわかっていない ● そこまでごまかせていたものが表出するだけ ● x = (x = x + 1) + x がわからないのも

Slide 13

Slide 13 text

逐次実行をちゃんとやろう ● パターンではなく、ちゃんと追えるようになろう ● そしてちゃんと書けるようになろう

Slide 14

Slide 14 text

ループ ● まずは、各回が独立したループを考える ● 今日の話は、最初は簡単と思っていても どこかからなぜか難しくなると思います

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

回数が決まっている ● 回数が決まったループは少し面倒 ● Cスタイルの構文の問題 void main() { for (int i = 0; i < 5; i++) { println("Hello"); } }

Slide 17

Slide 17 text

rangeを使って回数指定 ● IntStream.rangeを使うとわかりやすい ● Javaの場合は構文がちょっと複雑 import static java.util.stream.IntStream.range; void main() { range(0, 5).forEach(_ -> { println("Hello") }); }

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

ループによる値の集約 ● ループを使って合計 void main() { int[] data ={23, 76, 43, 9, 17, 32, 59}; var result = 0; for (int n : data) { result += n; } println(result); }

Slide 22

Slide 22 text

条件を全てが満たすかどうか ● ループで実装 void main() { int[] data ={23, 76, 43, 9, 17, 32, 59}; var result = true; for (int n : data) { result &= n % 2 == 1; } println(result); }

Slide 23

Slide 23 text

条件を満たす値のリスト ● ループで実装 ● 数値配列を動的に構築するときは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(); println(nums); }

Slide 24

Slide 24 text

集約の基本構造 ● 共通の構造がある ● 仕組み化できる ● Stream var result = 初期値 for (繰り返し) { 処理 resultへの追加処理 }

Slide 25

Slide 25 text

Streamによる値の集約 ● streamによる合計 ● pythonではmap-filter-reduce ● haskellではmap-filter-fold import java.util.stream.IntStream; void main() { int[] data ={23, 76, 43, 9, 17, 32, 59}; int result = IntStream.of(data) .sum(); println(result); }

Slide 26

Slide 26 text

条件を全てが満たすかどうか ● 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); println(result); }

Slide 27

Slide 27 text

条件を満たす値のリスト ● 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(); println(result); }

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

単位元 ● 初期値は単位元になっている ● 単位元 ● a = e a = a e ★ ★ となる値a ● 足し算の場合0 ● 掛け算の場合1 ● リストの結合の場合、空リスト 終端処理 初期値 更新処理 sum 0 + allMatch true & anyMatch false | toList new ArrayList() add

Slide 30

Slide 30 text

Streamの利点 ● for文が命令的なのに対してStreamは関数的 ● 関数的≒ 宣言的 ● 処理を追わなくてよい ● ニンゲンは処理の記述を追うのが苦手 ● つまり、処理を追わずにループが読める! for (int n : data) { if (n % 2 == 1) continue; result.add(n); } var result = IntStream.of(data) .filter(n -> n % 2 == 1) .toArray();

Slide 31

Slide 31 text

前後のデータに依存するループ ● Streamで書けない ● ただしJava 22ではGathererによって可能になる

Slide 32

Slide 32 text

移動平均 ● 他のデータを参照する ● 複数のデータで平均をとる ● 平均をとる範囲が移動していく ● なめらかにする ● ローカットフィルタ ● 例: 日次売上を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(); println(Arrays.toString(result)); }

Slide 33

Slide 33 text

隠れた状態を利用する ● データ直接ではないとき難しい

Slide 34

Slide 34 text

カッコの対応を判定する ● カッコの対応を判定 ● “abc(def)ghi” -> 🙆 ● “abc((def)ghi” -> 🙅‍♀️

Slide 35

Slide 35 text

カッコの対応を判定する処理 ● カウンタを使う

Slide 36

Slide 36 text

隠れた状態さえ見つければ実装は簡単 ● 割と素直 ● コードから難しさが わかりにくい 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; }

Slide 37

Slide 37 text

複雑な状態遷移 ● 状態が入り組むことがある ● 状態遷移としてまとめる

Slide 38

Slide 38 text

状態遷移図 ● 状態遷移を表す図 ● 例: 10進数整数

Slide 39

Slide 39 text

状態遷移のコード ● 状態遷移図の実装 ● 状態遷移図さえ書ければ コードは簡単 ● 状態遷移図がなければ 読み解くのは難しい 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; }

Slide 40

Slide 40 text

状態遷移はいろいろ現れる ● リクエストを受けるごとに状態が遷移 ● 例: 購入->入金->配送 ● 場当たり的にフラグ管理をして破綻しがち

Slide 41

Slide 41 text

正規表現と正規言語 ● 状態遷移コードは意図がわかりにくい ● 正規表現であらわせる ● 意図がわかりやすい boolean check(String s) { return s.matches("0|[1-9][0-9]*"); }

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

変数がわからないのは逐次実行がわかっていない ● 変数がかわるイメージがつかない void main() { var a = 2; println(a); a += 1; println(a); }

Slide 44

Slide 44 text

クロックで実行処理を決める ● プログラムカウンタを使う int counter = 0; void clock() { counter = (counter + 1) % 2; switch (counter) { case 0 -> println("hello"); case 1 -> println("world"); } } void main() { for (;;) clock(); }

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

再帰によるループ ● 再帰はループになる void main() { println("hello"); main(); }

Slide 47

Slide 47 text

回数の決まった再帰 ● 条件をいれて止める void loop(int i) { if (i -> 5) { return; } println(STR."Hello \{i}"); loop(i + 1); } void main() { loop(0); }

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

入れ子になった状態遷移 ● 複数のカッコの対応を判定 ● 再帰を使うと書きやすい ● スタックの隠蔽 void check(String str) { idx = 0; 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; }

Slide 50

Slide 50 text

文脈自由言語 ● 入れ子のある正規表現 ● 構文解析に使える

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

もっと勉強するには ● プロになるJava ● WEB+DB PRESS Vol.135, Vol.136 ● 数学ガール 乱択アルゴリズム

Slide 55

Slide 55 text

AI時代にも価値を出せるように ● ライブラリの使い方は「AI」がわかる ● 単純な知識の蓄積に意味がなくなる ● 検索で暗記の必要はなくなっていた ● 手に覚えさせることも不要になりつつある ● プログラムの処理が発想できること把握できることが大切 ● 正しく表現できる、読み取れることが大切 ● 発想したあとのコードは「AI」が書いてくれる