262 Views
September 30, 26
スライド概要
AI が生成したコードを読む負担を減らすため、 日本語でコードを書くことを提案し、 なでしこ3 を用いた FizzBuzz の例で TDD の Red‑Green‑Refactor サイクルを実演します。 また、 Nix や Makefile、 GitHub Actions を組み合わせた開発環境と自動化フロー、 辞書やクロージャを活用したクラス不要の構造化設計手法についても解説しています。
なんか地域の少し変わったおじさん
テスト駆動開発から始める なでしこ3 入門 AI 時代の「読む負担」を日本語コードで減らせるか 2026-09-30 1
AI に書かせると、人間は読まされる 減った負担 ・タイピング ・構文を思い出す ・定型コードを書く ・サンプルを探す 増えた負担 ・生成コードを理解する ・意図と実装を照合する ・誤りを見つける ・修正の影響を読む 人間のボトルネックは、書くことから読むことへ移った。 2
ならば、日本語でコードを書けばいいんじゃね? 仮説:日本語で書けば、生成コードを読む 認知負荷を下げられるのではないか。 評価軸 確かめること 可読性 日本語の語順と助詞から値の役割を追えるか 検証可能性 テストも日本語の仕様として読めるか 設計力 辞書・関数で変更に耐える構造を作れるか 実用性 lint・format・CIまで自動化できるか 3
最初に見てほしい「日本語のコード」 ●(Nを)FizzBuzz変換 Nを文字列変換して戻す ここまで 「1を渡したら文字列1を返す」と (1をFizzBuzz変換)と「1」で検証 ・(Nを)と 1 をが対応する ・を が値と処理の関係を示す ・テストが、そのまま日本語の仕様として読める 日本語なら、本当に読みやすく、正しく、育てやすいのか? 4
Python 参照:最初の関数とテスト なでしこ3 Python / pytest ●(Nを)FizzBuzz変換 Nを文字列変換して戻す ここまで 「1を渡したら文字列1を返す」と (1をFizzBuzz変換)と「1」で検証 def fizz_buzz(n: int) -> str: return str(n) def test_1を渡したら文字列1を返す(): assert fizz_buzz(1) == "1" ・Python は fizz_buzz(1) の位置で引数を渡す ・なでしこ3 は 1をFizzBuzz変換 の助詞で役割を示す 5
PART 1 TDD の基本サイクル 小さな仕様から始める 6
仕様を TODO リストへ分解する ・[] 1 を渡したら "1" ・[] 2 を渡したら "2" ・[] 3 の倍数なら Fizz ・[] 5 の倍数なら Buzz ・[] 15 の倍数なら FizzBuzz ・[] 1 から N までを一覧化する 一度に全部つくらない。次の一歩だけを選ぶ。 7
最初の Red 「1を渡したら文字列1を返す」と (1をFizzBuzz変換)と「1」で検証 [文法エラー] 関数『FizzBuzz変換』が見つかりません。 ・実装より先に、期待する振る舞いを書く ・関数が存在しないエラーも、立派な Red ・失敗を観察してから実装へ進む 8
Fake It — 最小の Green ●(Nを)FizzBuzz変換 「1」を戻す ここまで ・正しい一般解を急がない ・最初のテストを通すだけの仮実装 ・Green は次の変更に進める安全地点 「ズルでは?」――次のテストが、このズルを壊してくれる。 9
Triangulation — 例を増やす 「1を渡したら文字列1を返す」と (1をFizzBuzz変換)と「1」で検証 「2を渡したら文字列2を返す」と (2をFizzBuzz変換)と「2」で検証 ●(Nを)FizzBuzz変換 Nを文字列変換して戻す ここまで ・2つ目の例が定数返しを壊し、一般化を導く 10
条件の順番も仕様である ●(Nを)FizzBuzz変換 もし、N%15=0ならば、「FizzBuzz」を戻す もし、N%3=0ならば、「Fizz」を戻す もし、N%5=0ならば、「Buzz」を戻す Nを文字列変換して戻す ここまで ・15 は 3 の倍数でも 5 の倍数でもある ・FizzBuzz を先に判定しなければならない ・小さな例が、隠れた優先順位を明らかにする 11
Python 参照:FizzBuzz の条件分岐 def fizz_buzz(n: int) -> str: if n % 15 == 0: return "FizzBuzz" if n % 3 == 0: return "Fizz" if n % 5 == 0: return "Buzz" return str(n) def test_fizz_buzz(): assert fizz_buzz(3) == "Fizz" assert fizz_buzz(5) == "Buzz" assert fizz_buzz(15) == "FizzBuzz" ・条件の順番という仕様は、言語が変わっても同じ 12
Obvious Implementation ●(Nまで)FizzBuzz配列作成 結果 = [] Iを1からNまで繰り返す 結果に(IをFizzBuzz変換)を配列追加 ここまで 結果を戻す ここまで ・実装が明白なら、素直に書いてテストする ・仮実装だけが TDD ではない ・変換と一覧生成の責務を分ける 13
Python 参照:1 から N までを変換する なでしこ3 Python ●(Nまで)FizzBuzz配列作成 結果 = [] Iを1からNまで繰り返す 結果に(IをFizzBuzz変換)を配列追加 ここまで 結果を戻す ここまで def fizz_buzz_list(n: int) -> list[str]: result = [] for i in range(1, n + 1): result.append(fizz_buzz(i)) return result ・Nまで と range(1, n + 1) に境界表現の違いが出る 14
Red → Green → Refactor Red 失敗するテスト Green 最小の実装 Refactor 動作を保って改善 TODO がなくなるまで、小さく反復する。 15
第 1 部で得たもの 手段 得られたもの TODO リスト 大きな仕様を小さくする Test First 期待を実装より先に固定する Fake It 最小の Green を得る Triangulation 例から一般化を導く Refactor 動作を保ったまま構造を改善する 成果:日本語で読める、テストされた FizzBuzz 16
PART 2 開発環境と自動化 手元の成功を、チームの規律へ 17
テストが通ったら履歴を残す 変更確認 → 差分確認 → ステージ → コミット TDD の動き Conventional Commits テストを追加 test: 振る舞いを追加 feat: 構造を改善 refactor: 環境を整備 build: ・小さなテスト単位と、小さなコミット単位をそろえる ・履歴から「何を確かめ、何を変えたか」を読める 18
再現できる開発環境 nix develop .#nadesiko gonako --version make check ・gonako 3.8.8 を固定 ・Nix で処理系と Go のバージョンをそろえる ・誰もが同じ入口から、同じコマンドを実行する 「自分の環境では動く」を、環境定義で減らす。 19
4 つの品質チェック チェック コマンド 守るもの Unit Test make test 関数の振る舞い Lint make lint 文法・識別子・未定義語 Format make format-check 書式の一貫性 Doctest make doctest 表示・受け入れ結果 make check 単体テストだけでは、出力や整形差分までは守れない。 20
テストランナーを自分で育てる ファイルの中 ・検証 で成功・失敗を数える ・失敗内容を日本語で表示 ・最後に終了コードへ反映 ファイルの外 ・Makefile が *_test.nako3 を列挙 ・1 ファイルずつ実行 ・全体の成功・失敗を集計 テストの仕組み自体も、育てるプロダクトである。 21
CI はローカルの約束を繰り返す 開発者 GitHub Actions make check lint / format / test / doctest ・ローカルと CI で同じコマンドを使う ・push / pull request ごとに品質ゲートを再実行する 22
第 2 部で得たもの ・Git と Conventional Commits で変更の意図を残す ・Nix で処理系を固定する ・Makefile で品質チェックの入口を一つにする ・GitHub Actions で約束を繰り返す 成果:誰が実行しても同じ判定になる開発フロー 23
PART 3 構造化設計 クラスがなくても設計できる 24
新しい要求:変換タイプを切り替える ・通常:3 → Fizz ・数字限定:3 → 3 ・FizzBuzz 限定:3 → 3、15 → FizzBuzz 困ったこと ・条件分岐を利用側へ散らしたくない ・なでしこ3 にはクラス構文がない 使える道具 辞書・関数参照・無名関数・クロージャ 25
辞書によるポリモーフィズム ●(番号から)タイプ生成 もし、番号 = 1ならば {"名前": "通常", "変換": {関数}FizzBuzz変換}を戻す 違えば、もし、番号 = 2ならば {"名前": "数字限定", "変換": {関数}数字限定変換}を戻す ここまで ここまで ・属性は辞書の値 ・振る舞いは辞書に格納した関数 ・利用側は同じ 変換 だけを見る 26
Python 参照:辞書に関数を格納する
from collections.abc import Callable
type Converter = Callable[[int], str]
def create_type(number: int) -> dict[str, str | Converter]:
if number == 1:
return {"name": "通常", "convert": fizz_buzz}
if number == 2:
return {"name": "数字限定", "convert": str}
raise ValueError(f"該当するタイプは存在しません: {number}")
def convert(type_: dict, n: int) -> str:
converter = type_["convert"]
return converter(n)
・Python でも関数は値。辞書へ格納して差し替えられる
・なでしこ3 は {関数}FizzBuzz変換 で関数参照を明示する
27
パターンは問題から導入する 問題 パターン なでしこ3 での表現 数値と結果がばらばら Value Object 2 値を持つ辞書 生の配列を直接操作 First-Class Collection 配列を包む辞書と関数 操作ごとに呼び方が違う Command 実行 関数を持つ辞書 パターン名から始めない。テストで見えた責務から導入する。 28
Command とクロージャ ●(タイプで)値コマンド作成 処理 = 関数(N) [タイプでNからFizzBuzz値作成]を戻す ここまで {"名前": "値コマンド", "実行": 処理}を戻す ここまで ・無名関数が作成時の タイプ を覚える ・値コマンドもリストコマンドも配列を返す ・戻り値の形をそろえ、利用側の分岐をなくす 29
Python 参照:Command とクロージャ
from collections.abc import Callable
def create_value_command(type_: dict) -> dict[str, object]:
def execute(n: int) -> list[dict]:
return [create_value(type_, n)]
return {"name": "値コマンド", "execute": execute}
def execute(command: dict, n: int) -> list[dict]:
operation: Callable = command["execute"]
return operation(n)
・内側の execute が外側の type_ を覚える
・クロージャの役割は同じ。構文と呼び出し順が異なる
30
SOLID を構文ではなく責務として読む 原則 この実装での現れ方 SRP 変換・値・一覧・コマンドを分離 OCP タイプを追加しても利用側を変えない LSP どのタイプも同じ変換規約で置換可能 ISP 必要最小限の関数・辞書項目を公開 DIP 具体的な変換でなく、渡された関数へ依存 動的型付けでは、テストが契約を支える。 31
モジュール構成が設計図になる src/ ├── fizzbuzz.nako3 # 基本変換 ├── type.nako3 # タイプとディスパッチ ├── value.nako3 # 値オブジェクト ├── list.nako3 # コレクション └── command.nako3 # 操作 ・名前と配置で責務の境界を見えるようにする ・依存の向きを単純に保つ ・テストも責務ごとに分割する 32
第 3 部で得たもの ・クラス構文の有無と、よい設計は別問題 ・辞書と関数でデータと振る舞いを組み合わせる ・パターンと SOLID で変更の影響を閉じ込める ・テストを動的な契約として使う 成果:要求追加に開いた FizzBuzz の構造 33
PART 4 関数型スタイルへ 「何をするか」をつなぐ 34
関数を値として扱う 規則 = {関数}FizzBuzz変換 規則(15) ●(規則でNを)規則適用 規則(N)を戻す ここまで ・{関数} で関数そのものを取り出す ・引数として渡し、あとで呼ぶ ・名前付き関数・無名関数・クロージャを同じ値として扱う 35
map / filter / composition ●(規則でNまで)一括変換 規則を(1からNまでの配列連番作成)へ配列マップ ここまで ●(判定で配列を)絞り込み 判定で配列を配列フィルタ ここまで ・配列マップ:各要素を変換した新しい配列 ・配列フィルタ:条件に合う要素だけを残す ・ループを「対象に規則を適用する」という宣言へ変える 36
Python 参照:高階関数と map / filter from collections.abc import Callable, Iterable def apply_rule(rule: Callable[[int], str], n: int) -> str: return rule(n) def convert_all(rule: Callable[[int], str], n: int) -> list[str]: return list(map(rule, range(1, n + 1))) def select( predicate: Callable[[int], bool], values: Iterable[int] ) -> list[int]: return list(filter(predicate, values)) ・Python も関数を引数として渡せる ・規則でNまで一括変換 は、助詞で 2 つの引数の役割を分ける 37
Haskell 参照:高階関数と map / filter type Rule = Int -> String applyRule :: Rule -> Int -> String applyRule rule n = rule n convertAll :: Rule -> Int -> [String] convertAll rule n = map rule [1 .. n] select :: (a -> Bool) -> [a] -> [a] select = filter ・関数の型 Int -> String をそのまま別名にできる ・カリー化により、引数を 1 つずつ適用する ・map / filter は新しいリストを返す 38
日本語のパイプライン ●(Nを)FizzBuzz装飾 NをFizzBuzz変換して装飾して戻す ここまで 入れ子 「~して」 装飾(FizzBuzz変換(N)) NをFizzBuzz変換して装飾 内側から外側へ読む 処理順に左から右へ読む 39
パイプライン参照:F# / Elixir / Clojure F# |> Elixir |> Clojure -> let FizzBuzz装飾 n = n |> FizzBuzz変換 |> 装飾 def fizz_buzz_decorated(n) do n |> fizz_buzz() |> decorate() end (defn fizz-buzz-decorated [n] (-> n fizz-buzz decorate)) 値を次の関数の 最後の引数へ渡す 値を次の関数の 第 1 引数へ渡す スレッディングマクロが 値を先頭引数へ挿入する 3 言語とも左から右へ値が流れる。なでしこ3 は「~して」自体が日本語の接続として読める。 40
Python 参照:パイプラインと不変データ
def decorate(value: str) -> str:
return f"[{value}]"
def decorated_fizz_buzz(n: int) -> str:
return decorate(fizz_buzz(n))
def pipeline(n: int) -> list[str]:
return list(map(decorated_fizz_buzz, range(1, n + 1)))
def appended(values: list[int], x: int) -> list[int]:
return [*values, x]
・Python の関数合成は内側から外側へ読む
・なでしこ3 の「~して」は処理順に左から右へ読める
41
Haskell 参照:関数合成と不変データ decorate :: String -> String decorate value = "[" ++ value ++ "]" decoratedFizzBuzz :: Int -> String decoratedFizzBuzz = decorate . fizzBuzz pipeline :: Int -> [String] pipeline n = map (decorate . fizzBuzz) [1 .. n] appended :: [a] -> a -> [a] appended values x = values ++ [x] ・(.) が右から左へ関数を合成する ・値は変更せず、常に新しい値を返す ・不変性は規約ではなく、言語の基本モデル 42
不変データで流れを追いやすくする ●(配列にxを)追加した配列 新配列 = 配列を配列複製 新配列にxを配列追加 新配列を戻す ここまで ・元の配列を変更せず、新しい配列を返す ・各段階が「入力 → 新しい出力」になる ・テストで元データが変わらないことも確かめる 変更箇所を探す負担が減り、処理の流れを追いやすくなる。 43
エラーも値として返す {"成功": はい, "値": 値} {"成功": いいえ, "エラー": メッセージ} ●(Nを)安全変換 もし、(Nの変数型確認) ≠ 「number」ならば 「数値を指定してください: {N}」の失敗結果を戻す ここまで (NをFizzBuzz変換)の成功結果を戻す ここまで ・成功と失敗を同じ形の結果辞書で扱う 44
Python 参照:結果を値として返す
from typing import TypedDict
class Result(TypedDict, total=False):
success: bool
value: str
error: str
def safe_convert(value: object) -> Result:
if not isinstance(value, int):
return {"success": False, "error": f"数値を指定してください: {value}"}
if value <= 0:
return {"success": False, "error": f"正の数を指定してください: {value}"}
return {"success": True, "value": fizz_buzz(value)}
・Python では isinstance、なでしこ3 では 変数型確認
・どちらも成功/失敗を呼び出し側が明示的に扱える
45
Haskell 参照:Either で失敗を型にする safeConvert :: Int -> Either String String safeConvert n | n <= 0 = Left ("正の数を指定してください: " ++ show n) | otherwise = Right (fizzBuzz n) strictConvert :: Int -> String strictConvert n = case safeConvert n of Right value -> value Left message -> error message ・Right が成功、Left が失敗 ・呼び出し側はパターンマッチで両方を扱う ・引数が Int なので、文字列の誤入力はコンパイル時に除外される 46
動的型付けで安全性をつくる 1. 関数の入口で 変数型確認 2. 想定外の値を失敗結果にする 3. 厳格変換 で失敗を例外へ変換できる 4. 空許容変換 で Option 相当を表現する 5. 値だけでなく型もテストする 型安全性は「静的型の有無」だけでなく、境界設計の問題でもある。 47
Option 相当を表現する なでしこ3:空 Scala : Option Kotlin : null 許容型 ●(Nを)空許容変換 もし、N = 空ならば 空を戻す ここまで NをFizzBuzz変換して戻す ここまで def 空許容変換( 値: Option[Int] ): Option[String] = 値.map(FizzBuzz変換) val なし = 空許容変換(None) val あり = 空許容変換(Some(3)) fun 空許容変換( 値: Int? ): String? = 値?.let(::FizzBuzz変換) val なし = 空許容変換(null) val あり = 空許容変換(3) ・3つとも「値がない」状態を表現する ・Scala と Kotlin は、値がない可能性を型に含める ・失敗理由が必要なら Either・Result・判別共用体を使う 48
静的型付けで安全性をつくるには
F#:判別共用体で成功/失敗を表現する
type 変換結果 =
| 成功 of string
| 失敗 of string
let 安全変換 (値: int) : 変換結果 =
if 値 <= 0 then
失敗 $"正の数を指定してください: {値}"
else
成功 (FizzBuzz変換 値)
let 結果表示 結果 =
match 結果 with
| 成功 値 -> 値
| 失敗 メッセージ -> $"エラー: {メッセージ}"
・成功と失敗以外の状態を作れない
・パターンマッチで両方を扱わないと、コンパイラが警告する
・値: int により、文字列の誤入力はコンパイル時に除外される
49
第 4 部で得たもの ・関数を値として渡し、振る舞いを組み立てる ・パイプラインで処理順を左から右へ読む ・不変データで変更の追跡を簡単にする ・エラーを値にして、失敗も合成する 成果:日本語の語順を活かした宣言的な FizzBuzz 50
12 章で育ったもの 部 出発点 到達点 TDD の基本 未定義の 1 関数 テストされた FizzBuzz 開発環境 手元での実行 再現可能な品質ゲート 構造化設計 条件分岐 交換可能な設計 関数型スタイル 可変なループ 合成可能なパイプライン 例 → テスト → 実装 → 自動化 → 設計 → 合成 51
最大の気づき 「日本語」より「助詞」が効いていた FizzBuzz変換(3) # 役割を関数名と位置から推測 3をFizzBuzz変換 # 「を」が値と変換の関係を示す 検証(名前, 実際, 期待) 名前と実際と期待で検証 # 「と」「で」が役割を示す 助詞は飾りではない。コード理解のインターフェースとして働く。 52
でも、日本語の識別子なら他言語でも書ける 日本語化スコア: なでしこ3 と 17 言語平均 ・ほかの 17 言語でも実装して比較 ・17 言語中 16 言語で日本語識別子を利用可能 ・平均が低いのは名前の 2 軸 ・語順とテスト名は言語機能で補いやすい 各軸 0~3 点。出典:多言語統合解説 第 9 章 53
メジャー言語でも「助詞らしい見た目」は作れる // Kotlin infix fun <T, R> T.を(動詞: (T) -> R): R = 動詞(this) infix fun <T, R> T.して(動詞: (T) -> R): R = 動詞(this) fun FizzBuzz装飾(数: Int): String = 数 を ::FizzBuzz変換 して ::装飾 ・TypeScript:メソッドチェーン ・Python:演算子多重定義 ・Rust:宣言的マクロ ・F# / Elixir:パイプライン 54
決定的な違い 助詞を「置ける」か、言語が「理解する」か ・他言語:助詞を関数・演算子・マクロとして自作する ・なでしこ3:●(Nを)FizzBuzz変換 が言語の文法 ・を・に・でが、引数の役割を決める ・Unicode の日本語名と、日本語の文法は同じではない 日本語で命名できる言語は多い。 日本語の文法で書けるのが、なでしこ3 の違いである。 55
日本語化には代償もある 観点 起きること 大文字・小文字 公開規則・型名・定数名と衝突 lint ASCII 前提の命名規則を調整する必要 ロケール UTF-8 でないとファイル名やテスト検出が壊れる Unicode 正規化 見た目が同じ名前が同一/別物になり得る 開発体験 IME、検索、補完、全角・半角の揺れ 書けることと、チームで書くべきことは分けて考える。 56
評価結果:読む負担は減るのか ・読解:助詞が値の役割を示し、処理を日本語順に追える ・検証:テスト名と検証式が日本語の仕様になる ・設計:辞書と関数で変更可能な構造を作れる ・自動化:lint・format・doctest・CI まで構築できる ・限界:境界条件・副作用・型の誤りは消えない 読む負担を下げたのは日本語の単語だけではなく、助詞による関係の明示だった。 57
もっと詳しく知りたいなら テスト駆動開発から始める なでしこ3 入門 ・TODO リストから始める Red → Green → Refactor ・Git・Nix・Makefile・GitHub Actions による開発環境 ・辞書と関数によるポリモーフィズム、デザインパターン、SOLID ・高階関数、不変データ、パイプライン、エラーハンドリング ・全 12 章の解説と実行可能なサンプルコード https://k2works.github.io/getting-started-tdd/article/nadesiko/ テストを手がかりに、日本語コードの設計を段階的に育てます。 58
ご清聴ありがとうございました 日本語で読める。テストで信頼できる。 59