015 なでしこ3 版の処理系・テスト・静的検査の選定¶
「機械学習から始めるプログラミング入門」の番外編(Extra)、なでしこ3 版で使う処理系と道具を決める。
日付: 2026-09-28
ステータス¶
2026-09-28 提案されました
2026-09-28 承認されました(第 1〜15 章の執筆で確かめました。実データでの実測はまだです)
2026-09-28 main への pull request で Nadesiko3 CI が初めて走り、Nix の中で GOTOOLCHAIN が効いて Go 1.26.8 のツールチェーンを取り、check.sh がすべて通ることを確かめました
コンテキスト¶
執筆計画 の「Extra: なでしこ3(nadesiko3go)版執筆計画」では、14 言語で完成したシリーズに、日本語プログラミング言語「なでしこ3」の版を番外編として加えるとした。なでしこ3 の公式実装は TypeScript 版で、本シリーズでは Go で書かれた実装 nadesiko3go(コマンド名 gonako)を使う。インストールせずに動く単一のバイナリになり、Go 版と同じ土台の上で読み比べられるためである。
B76 のステップ 1 で、nadesiko3go を手元でビルドし、スクラッチパッドの使い捨てのスクリプトで次を確かめた。
| 確認したこと | 結果 | 確認方法 |
|---|---|---|
| 処理系 | nadesiko3go 3.8.8(2026-09-24 のタグ)、MIT License。2026-09-28 にもコミットがあり、活発に保守されている | GitHub のリポジトリ、gonako version |
| Go の版 | go.mod が go 1.26.0 を要求する。flake.lock が固定する nixpkgs の go は 1.25.5、go_1_26 は 1.26rc1 で、どちらも要求を満たさない。nixpkgs の Go は既定で GOTOOLCHAIN=local なので、そのままでは requires go >= 1.26.0 (running go 1.25.5; GOTOOLCHAIN=local) で止まる。GOTOOLCHAIN=go1.26.8 を指定すると 1.26.8 のツールチェーンを取得して入る |
Go 1.25.5 で再現 |
| 版の指定 | タグが v の付かない 3.8.8 なので Go のモジュールの版として読めず、@3.8.8 はコミットに解決されて疑似版 v0.0.0-20260924163730-f840295acd3d になる |
go install の出力 |
| テスト | 専用のテスティングフレームワークは無い。AとBがASSERT等 は失敗すると終了コード 1 で止まる。最初の失敗で止まるので、ほかのテストの結果が分からない |
使い捨てのスクリプト |
| エラー | 「…」のエラー発生 で失敗を起こし、エラー監視 〜 エラーならば 〜 ここまで で捕まえられる。捕まえなければ終了コード 1 になる |
同上 |
| 静的検査 | gonako lint は関数名の打ち間違いを捕まえる(終了コード 1)。1 回に 1 ファイルしか受け付けない。gonako format は整形の結果を出力するだけで、検査用のモードが無い |
わざと崩したファイル |
| doctest | gonako doctest は .txt だけを対象にし、.md を渡すと「対象のサンプルコードがありません」で終了コード 0 になる(黙って成功する) |
Markdown とテキストで実行 |
| カバレッジ・型検査 | どちらも無い | gonako --help |
| 数値 | float64 だけ。「170cm」+0 が 170 になる(先頭の数字だけ読む)。「高い」+0 は NaN |
使い捨てのスクリプト |
| CSV | CSV取得 は BOM を取り除かず、空行を「空文字列 1 つだけの行」として読む |
BOM 付きのファイルで確認 |
| 乱数 | 乱数 は Go の rand.Float64() を呼び、シードを渡す口が無い |
internal/mathlib/mathlib.go |
| 機械学習・行列 | どちらの命令も無い(行列の命令は転置の 表行列交換 だけ) |
gonako doc |
各章で確かめた結果¶
| 章 | 確かめたこと |
|---|---|
| 1 | テスト補助を自作した(ASSERT等 は最初の失敗で止まる)。識別子に助詞を入れられない(ルールで判定 が「ルール」の定義に割られる)。lint はかっこの中や値の位置の未定義の名前を素通りさせる(undefined になる)。!厳チェック は構文として受け付けるだけ。「170cm」+0 が 170 になるので正規表現で形を確かめる。書式指定の命令が無いので小数表示を自作した |
| 2 | 24 ビットずつに分けた線形合同法で、シード 0 の nextInt(100) が [60, 48, 29, 47, 15]、[0..9] の並べ替えが [4, 8, 9, 6, 3, 5, 2, 1, 7, 0] になり、Java 版・Elixir 版・PHP 版・Haskell 版と一致した。上位 24 ビットが 0 でないシード(12345678901)も Python の参照実装と一致した。XOR は 32 ビットの符号付き整数で計算するので初期化も 24 ビットずつ行う。= は JavaScript の == と同じゆるい比較で 0 = 「」 が真になるため、空欄の判定は === にした。辞書はキーを入れた順を保つ。自作のアヤメ風データで、分割と補完前の平均 4 列が Python の参照実装と最後の桁まで一致した |
| 3 | 決定木を辞書で表した。表数値ソート は安定(sort.SliceStable)で、元の表を書き換える。引数を 終了 と名付けると、プロセスを終わらせる命令が呼ばれ、エラーも出さず終了コード 0 で止まるので、check.sh で検査結果報告まで届いたかを確かめるようにした。の間 の条件はかっこで囲まないと、lint は通るのに実行で「『while』はまだ実行に対応していません。」になる。自作のアヤメ風データで、深さごとの正解率と深さ 2 の木の報告が Python の参照実装と 1 文字も違わず一致した |
| 4 | gonako は CRLF のファイルをそのまま実行し、lint も通し、整形も CRLF のまま出力するので、改行を守るのは .gitattributes だけ。環境変数取得 は設定が無いと空文字列を返す(「無い」と「空」を区別できない)。再現性のテスト(シードが違えば並びや分割が変わる)を足し、シードを同じにすると落ちることを確かめて空振りしていないことを示した |
| 5 | カバレッジの代わりに「関数の網羅」をなでしこ3 で自作した(tools/function_coverage.nako3)。テストから直接・間接に名前が現れる関数を数え、長い名前の一部として現れる短い名前は数えない。48 件中 48 件(直接 38・間接 10)。命令の引数が足りないと、足りない引数が直前に計算した別の値で埋められ、呼ぶ場所によって結果が変わる(文字検索 の「から」を省いた。lint は捕まえない)。「届く」は「通る」の必要条件でしかなく、データのあるときだけ走る分岐から呼ばれる報告の関数に、自作の小さなデータのテストを足した |
| 6 | 5 つの検査(文法・整形・テスト・テストが最後まで走ったか・関数の網羅)を check.sh にまとめ、Gulp と CI はそれを呼ぶだけにした。終了コードはすべて 1。5 つとも壊して落ちることを確かめた。章の実行(main.nako3)は実行時エラーで終了コード 1 になる |
| 7 | 行列の転置・積と部分ピボット付きのガウスの消去法を src/linear_algebra.nako3 に自作した(表行列交換 は行の長さがそろわないと「」で埋めてエラーにしない)。「の」で呼ぶ命令は直前の式全体にかかり、総和 + (X)の絶対値 が (総和 + X)の絶対値 になって MAE だけが静かにずれた(lint は通る)。近似確認 が差が NaN のとき合格にしていたので直した。自作の映画風データで、切片・係数・R²・MAE・RMSE が Python の参照実装と最後の桁まで一致し、有理数の厳密解とは相対誤差 1e-14 程度、ガウス・ジョルダン法とは 13〜14 桁目から違った |
| 8 | 前処理と学習済みの前処理を「種別」を持つ辞書で表し、学習済みのパイプラインを JSONエンコード と 保存 で保存した。JSONデコード は後ろのごみを黙って無視し、変数型確認 は辞書・配列・NULL をどれも object と答えるので、読み込んだ JSON の形・形式の版・余りを自分で確かめる。命令の上限(2 億)に、素直に書いた重み付きの決定木が実データと同じ大きさの自作データで届いたので、足す順を変えずに合計をその場で数える形にした(報告 1 回が約 4〜5 千万命令)。引数を 対象 と名付けると値が入らない(システムの変数)。自作の 20 行と 891 行のデータで、報告と保存したモデル(木の境界と不純度を含む)が参照実装と最後の桁まで一致した |
| 9 | 標準化は学習(平均と n で割る標準偏差を覚える)と適用を分けて自作し、統計の命令が無いので、手で計算できる標本と第 13 章の ボストン標準化 を突き合わせの相手にした。文字コードを取り違えるとどちら向きもエラーにならない(Shift_JIS を 開く で読むと読めないバイトが残り、JSONエンコード で初めて � になる。UTF-8 を SJISファイル読 で読むと U+FFFD の混じった別の文字になる)。漢字に挟まれた送り仮名は名前から落ちる(外れ値試験 と 外値試験 は同じ関数)。TSV取得 は CSV取得 と同じ読み取りで、BOM と空行は自分で取り除く。範囲 は組み込み。自作のボストン風データで、報告と決定係数 8 個が参照実装と最後の桁まで一致した(命令の数は 第9章実行 で約 149 万) |
| 10 | 無名関数を辞書に入れて「学習して予測する関数を返す」約束を表した(無名関数はクロージャになる。辞書["学習"](…) とは書けない)。EXP・LOG は止まらずに無限大と NaN を返し、JSONエンコード するとどれも null になる。EXP・LOG は Go の数学関数で、C の数学ライブラリとは最後のビットが違うことがある。重要度は Java 版の側に落ちる(2 通りの参照実装で分かれる自作データで確かめた)。命令数の上限(2 億)に対し、素朴な書き方の報告は約 5.6 億命令だった。 第 3 章の分割探索を、足す順を保ったまま数え上げに書き直し(約 858 万 → 約 179 万命令)、ロジスティック回帰を変数への足し込み・表行列交換・表列合計 で書き直して(5000 回で約 4 億 → 約 1.26 億命令)、報告全体を約 1.6 億命令にした。計算の結果は素朴な書き方と丸ごと一致することをテストで守る。余裕を残すため、報告はロジスティック回帰(chapter10-logistic、約 1.3 億)とそれ以外(chapter10、約 3 千万)を別々の実行で出す。次 は変数名に使えず、配列合計 は NaN を黙って読み飛ばす |
| 11 | 評価関数(関数(正解一覧, 予測一覧))と学習器(関数(特徴量一覧, 正解一覧))を無名関数で渡し、クロージャで正例と列名を覚えさせた。名前を付けた関数を値の位置に書くと、その場で引数の無いまま呼ばれ(lint も通る)、戻り値が入るので、無名関数で包んで渡す。辞書の鍵にした数値は、元に戻せる最短の文字列になり(0.1+0.2 と 0.3 は別の鍵、1 と「1」は同じ鍵)、ROC 曲線は並べてから同じスコアの範囲をまとめる形にした。配列の範囲の外を読むと undefined が返る。値の分かっている入力(AUC の 1・0.5・0、組の数え上げによる別定義)と満たすべき性質(全行を 1 度ずつテストする、重ならない、訓練データの並び順)で支え、自作の 891 行・100 行のデータで報告と 6 指標が参照実装と最後の桁まで一致した。第11章実行 は約 1945 万命令(テストファイルは約 4042 万)で、書き直しは要らなかった |
| 12 | リッジ回帰(中心化した正規方程式の対角に alpha を足し 連立一次方程式求解 で解く)とラッソ回帰(Haskell 版と同じ打ち切り 1e-10・上限 1 万巡・更新の順・軟しきい値)を自作した。突き合わせる相手が無いので、alpha 0 と第 7 章の線形回帰の一致、L2 ノルムの単調な減少、Xᵀ(t − Xw) = αw、KKT 条件をテストにした。自作のボストン風データ 6 通りで、報告と完全な精度の 37 個の値が参照実装とビット単位で一致した。第12章実行 は約 223 万命令(ラッソ回帰は 1 巡約 1.5 万命令、上限の 1 万巡なら約 1.5 億)。巡回(回)と 違反(違)は変数名に使えず、定義の先頭の行を指す分かりにくいエラーになる。Haskell 版の外れ値のテストは標準偏差を n で割っても通るので、境界の値のテストを足した |
| 13 | ヤコビ法による固有値分解を自作し、突き合わせる相手の代わりに A v = λ v・固有ベクトルの正規直交・固有値の和とトレースの一致をテストにした(トレースの一致だけでは回転の向きの誤りを見逃した)。打ち切りは Haskell 版と同じ上限 100 巡・非対角 2 乗和 1e-30 で、自作の 100 件・15 列のデータで 8 巡、1 回約 0.04 秒。参照実装と報告も固有値もビット単位で一致した。関数を定義と違う助詞で呼ぶと、トップレベルでは lint も実行も通り、直前に計算した値が引数に入る。符号 は組み込みで代入できず、-(式) は解析エラー、システム時間ミリ秒 は実行中に進まない |
| 14 | 素朴な K-means は報告に 4.7 億命令を要し、命令の上限に届いた。 計測用に作り直した処理系で測り、Hamerly の方法・表列合計(Go で書かれた命令で、0 から前の行へ順に足す)・6 列の距離の展開で 1.4 億命令にした。速い書き方が素朴な書き方と同じ結果になることを同点の多い格子点 48 通りで、報告を自作の 14 行と架空の 440 件 5 通りで参照実装と 1 文字も違わないことを確かめた。二乗 は組み込みの命令 |
| 15 | HTTP サーバーの命令が無いので、要求 {"method", "path", "body"} を JSON で標準入力から受け取り、応答 {"status", "headers", "body"} を JSON で返すコマンドにし、経路・検証の理由・ステータスコード(200・400・404・405・422・500・503)をほかの言語版の API とそろえた。置き場とモデルの約束を無名関数の辞書で、失敗をエラーではなく結果の辞書で表し、約束のテストを本物と偽物の置き場の両方に走らせた。取り込みは名前空間を作らないので層ごとにファイルを分けても名前は一意にする。種類 = 「数値」で戻る は代入と読まれて実行時に止まり、(値の切捨)(書式は「を」)は undefined になって判定が静かに偽になる(どちらも lint は通る)。表示 は代入できない。無いキーを読むと undefined(=== NULL は偽)。JSONデコード の後ろのごみは、送られてきた要求では捕まえられないので、その振る舞いをテストに残した。架空の映画 100 件・乗客 891 件で、保存したモデルの予測値 7497.291121711996 と生存の予測が参照実装と一致した。学習と保存は約 2522 万命令、予測 1 回は約 2 万命令以下。実装を 8 通り壊してどれもテストが落ちた |
実データ(配布データ)での数値は、学習データの無い環境で進めたため、どの章もまだ実測していない。CI(Nadesiko3 CI)は main への pull request で初めて走り、Nix の Go 1.25.5 から go: downloading go1.26.8 で gonako v3.8.8 を入れ、check.sh がすべて通った(学習データの無いランナーなので、実データのテストは保留)。
決定¶
| 用途 | 採用 | バージョン | ライセンス | 初出 |
|---|---|---|---|---|
| 言語・処理系 | nadesiko3go(gonako) |
3.8.8(疑似版 v0.0.0-20260924163730-f840295acd3d) |
MIT | 第 1 章 |
| 処理系の導入 | Nix の nadesiko3 環境の Go(1.25.5)から、GOTOOLCHAIN=go1.26.8 で go install する。入れ先は apps/nadesiko3/bin/(コミットしない) |
Go 1.26.8 | BSD-3-Clause | 第 1 章(記事での解説は第 5 章) |
| テスト | 自作のテスト補助(test/helper.nako3)。検査の件数と失敗を数え、最後に失敗があれば エラー発生 で終了コード 1 にする。実データのテストはデータが無ければ保留にする |
— | — | 第 1 章 |
| テストの実行 | tools/check.sh が test/*_test.nako3 を 1 つずつ実行し、終了コードと最後の報告の行を確かめる |
— | — | 第 1 章 |
| カバレッジの代わり | 自作の関数の網羅(tools/function_coverage.nako3)。テストから届かない関数が 1 つでもあれば失敗する |
— | — | 第 5 章 |
| 静的検査 | gonako lint を全ファイルに 1 つずつかける |
3.8.8 | MIT | 第 1 章(記事での解説は第 5 章) |
| 整形 | gonako format の出力と元のファイルの差分が無いことを確かめる |
3.8.8 | MIT | 第 1 章(記事での解説は第 5 章) |
| カバレッジ・型検査 | 無し。記事でそのことを扱う | — | — | 第 5 章 |
| CSV | CSV取得(BOM と空行は自分で取り除く) |
3.8.8 | MIT | 第 1 章 |
| 乱数 | java.util.Random と同じ線形合同法を自作する(float64 に収まるよう乗算を分ける) |
— | — | 第 2 章 |
| 線形代数・機械学習 | すべて自作する | — | — | 第 3 章以降 |
| 直列化 | JSONエンコード・JSONデコード |
3.8.8 | MIT | 第 8 章 |
| API | JSON の標準入出力でやりとりするコマンド(HTTP サーバーの命令が無いため) | — | — | 第 15 章 |
- 処理系は疑似版で固定する。
@3.8.8と書いても動くが、解決されるコミットはタグの付け替えで変わりうる。疑似版はコミットのハッシュを含むので変わらない - flake.lock は更新しない。 更新すればほかの 14 環境の Go・GHC・JDK などもまとめて上がり、各言語版の数値や検査の結果が変わりうる。なでしこ3 版の環境だけ
GOTOOLCHAINで切り替える - テストは
ASSERT等ではなく自作のテスト補助で書く。ASSERT等は最初の失敗で止まり、残りのテストの結果が分からない。テスト補助は失敗を数えて続け、最後にまとめて報告してから失敗させる。xUnit 系のフレームワークが最初に作ったものを、自分で小さく作ることになる - 関数名・変数名は日本語にする。 なでしこ3 を選ぶ理由そのものである。関数名は取り込んだファイルをまたいで一意にする(同じ名前が 2 つあると衝突する)
- 記事の検査に
gonako doctestを使わない。 Markdown を対象にせず、しかも黙って成功する。記事の表示結果は実装の実測値から転記する - gonako には 1 回の実行につき 2 億命令の上限がある(
internal/vm/vm.goのMaxInstructions。引数でも環境変数でも変えられず、超えると終了コード 1 で止まる)。gonako gengoで Go に変換すると上限を避けられるが、ファイルを読む関数(非同期関数)を含むと変換できない。計算の結果(ほかの言語版と一致する数値)を変えずに、書き方で上限に収める(2026-09-28、B79 の途中でユーザーが決定)。実データと同じ大きさの自作データで、章の実行 1 回とテストファイル 1 つがそれぞれ 1.5 億命令以下になることを目安にする - 機械学習のアルゴリズムと線形代数はすべて自作が最終実装になる。 「ライブラリへの置き換え」の節は「ほかの言語版の数値との突き合わせ」の節に差し替える
章ごとの方針¶
| 章 | 置き換え | 方針 |
|---|---|---|
| 1〜2 | 無し | CSV の BOM・空行・数値の読み取りを自分で書く。乱数は線形合同法を自作する |
| 3 | 無し | 自作の決定木が最終実装。ほかの言語版の数値と突き合わせる |
| 4〜6 | 無し | 道具立ての章。疑似版による固定・lint・整形の差分・自作のテスト補助・CI を扱い、カバレッジと型検査が無いことを書く |
| 7 | 無し | 正規方程式を部分ピボット付きのガウスの消去法で自作する |
| 8〜11 | 無し | 前処理・ロジスティック回帰・ランダムフォレスト・評価指標・交差検証を自作する |
| 12 | 無し | リッジ(正規方程式)とラッソ(座標降下法)を自作する |
| 13 | 無し | ヤコビ法で固有値分解を自作する |
| 14 | 無し | K-means を自作する |
| 15 | 無し | JSON の標準入出力でやりとりする予測コマンド |
検討した代替案¶
| 代替案 | 採用しなかった理由 |
|---|---|
公式の TypeScript 版(cnako3) |
Node.js が要り、TypeScript 版と同じ実行環境になる。Go で書かれた単一バイナリのほうが Go 版と並べて読める |
| flake.lock を更新して Go 1.26 を得る | ほかの 14 環境にも影響する。しかも固定中の nixpkgs の go_1_26 は rc 版である |
Nix の buildGoModule で gonako をパッケージにする |
Go 1.26 が nixpkgs に無い問題は同じで、vendorHash の管理が増える |
ASSERT等 だけでテストを書く |
最初の失敗で止まる。1 つのファイルに複数のテストを書くと、どれが落ちているかの全体が分からない |
gonako doctest で記事を検査する |
Markdown を対象にせず、黙って成功する |
| 識別子を英語にする | なでしこ3 を番外編に選んだ理由(日本語で手順を読ませる)が失われる |
影響¶
- 良い影響: 機械学習の手順が日本語の文として読める。関数名がそのまま用語集になる
- 良い影響: テスト補助・線形代数・乱数を自分で作ることで、ほかの版ではライブラリが隠していた部分が見える
- 悪い影響: カバレッジも型検査も無い。 テストを章ごとに厚くし、ほかの言語版の数値との突き合わせを受け入れ基準に含めて補う
- 悪い影響: 処理系の導入のたびに Go 1.26.8 のツールチェーンをネットワークから取得する。 Nix の環境だけでは閉じない。CI では Go のモジュールとツールチェーンをキャッシュする
- 悪い影響: 処理系が活発に変わっており、版を上げると書き方が動かなくなる可能性がある。版を上げるときは全章のテストを通してから上げる