第 5 章: パッケージ管理と静的解析¶
5.1 はじめに¶
前の章では、何をコミットし何をコミットしないかを決めました。この章では、処理系の版をどう固定するかと、コードをどう検査するかを見ていきます。
なでしこ3 版の検査は 5 つです。
| 検査 | 道具 | 違反したときの終了コード |
|---|---|---|
| 文法 | gonako lint |
1 |
| 整形 | gonako format の出力と元のファイルの diff |
1(diff の値) |
| テスト | 自作のテスト補助(エラー発生) |
1 |
| テストが最後まで走ったか | check.sh が出力の最後の行を読む |
1(自分で決めた) |
| 関数の網羅 | 自作(tools/function_coverage.nako3) |
1(自分で決めた) |
Haskell 版の 100・1・1・3 と違って、すべて 1 です。道具の作者が決めた値が gonako lint の 1 つしかなく、残りは diff か自分で決めた値だからです。
この章の見どころは 3 つです。
- 依存が 1 つも無い。管理する版は処理系(
gonako)とそれを入れる Go のツールチェーンの 2 つだけです - lint が素通りさせる誤りが多い。第 1〜3 章で見つけたものに、この章で見つけた「引数の不足」を加えて 5 つ並べます
- カバレッジの仕組みが無い。その代わりに、テストから届く関数を静的に数える検査をなでしこ3 で自作します
5.2 依存と版の管理¶
依存が 1 つも無い¶
なでしこ3 版は外部のライブラリを使いません。CSV の読み込み(CSV取得)・JSON(JSONエンコード)・正規表現・ファイルの読み書きは、すべて処理系に組み込まれた命令です。乱数・線形代数・機械学習のアルゴリズムは自作します(ADR 015)。
| 言語版 | 依存を書くファイル | ロックファイル |
|---|---|---|
| PHP 版 | composer.json |
composer.lock |
| Haskell 版 | *.cabal |
無し(置かない判断) |
| なでしこ3 版 | 無し | 無し(依存が無い) |
処理系の版は疑似版で固定する¶
管理する版は、処理系の gonako だけです。tools/install-gonako.sh に書いてあります。
readonly GONAKO_VERSION="v0.0.0-20260924163730-f840295acd3d" # タグ 3.8.8
readonly GO_TOOLCHAIN="go1.26.8"
GOTOOLCHAIN="${GO_TOOLCHAIN}" GOBIN="$(pwd)/bin" \
go install "github.com/kujirahand/nadesiko3go/cmd/gonako@${GONAKO_VERSION}"
第 1 章で見たとおり、nadesiko3go のタグは v の付かない 3.8.8 なので、Go のモジュールの版としては読めません。タグが指すコミットの疑似版で固定しています。疑似版にはコミットのハッシュ(f840295acd3d)が含まれるので、同じ名前のまま中身が変わることはありません。
Go の既定の設定(GOSUMDB=sum.golang.org)では、go install が取ってきたモジュールを公開のチェックサムのデータベースと照らし合わせます。ロックファイルを置かなくても、「同じ版なら同じ中身」は Go の仕組みが確かめてくれます。
Go のツールチェーンも固定する¶
nadesiko3go は Go 1.26 を要求し、Nix の Go は 1.25.5 です。GOTOOLCHAIN=go1.26.8 で、導入のときだけ 1.26.8 のツールチェーンに切り替えます。これも版の固定です。 GOTOOLCHAIN=auto にすると「1.26 以上のどれか」が選ばれ、日によって違う版でビルドされることになります。
版を上げるときは、install-gonako.sh の 2 行を書き換えて、全章のテストを通してからコミットします(build(nadesiko3): ...)。処理系は活発に更新されていて、2026 年 9 月だけで 3.8.1 から 3.8.8 まで 8 回、新しい版が出ています。
ファイルを取り込む¶
なでしこ3 のプログラムは、!「…」を取り込む でほかのファイルを読み込みます。
!「./csv.nako3」を取り込む
!「./dataset.nako3」を取り込む
第 1 章で見たとおり、./ を省くとパッケージの名前として探され、同じフォルダにあっても見つかりません。 なでしこ3 には gonako-package というパッケージの置き場の仕組みもありますが、この版では使っていません。取り込むのはすべて自分のファイルです。
取り込んだファイルの関数は、ファイル名の名前空間に入りますが、呼ぶ側は名前だけで呼べます。そのため、同じ名前の関数を 2 つのファイルに書くと衝突します。第 1 章の 特徴量正解分離 と第 2 章の 目的列分離 のように、関数名をファイルをまたいで一意にしています。
5.3 コードスタイル — gonako format¶
gonako format は、プログラムを実行せずに見た目だけを整えます。
| 整えるもの | 例 |
|---|---|
| 関数定義のかっこ | ●(値を桁数で)小数表示とは → ●(値を桁数で)小数表示とは |
| 繰り返しの書き方 | I を 0 から 5 まで繰り返す → Iを 0から 5まで繰り返す |
| 字下げ | 4 文字にそろえる |
| 演算子の前後 | 空白を 1 つ置く |
2 回かけても結果は変わりません。ただし検査用のモードがありません。 -f を付けなければ整形後の内容を表示するだけで、整形が要るかどうかを終了コードで教えてくれません。check.sh は、表示された内容と元のファイルの差分を取って判定します。
./bin/gonako format src/number_format.nako3 | diff -u src/number_format.nako3 -
--- src/number_format.nako3
+++ -
@@ -4,7 +4,7 @@
-●(値を桁数で)小数表示とは
+●(値を桁数で)小数表示とは
差分があれば diff が終了コード 1 を返します。整形の検査の終了コードは、整形の道具ではなく diff が決めています。
第 4 章で見たとおり、gonako format は CRLF をそのまま残します。改行は整形の対象外です。
5.4 静的解析 — gonako lint とその限界¶
lint が捕まえるもの¶
gonako lint は文法を検査します。関数名を打ち間違えて、かっこの外で命令として書いた場合は捕まえます。
[文法エラー]main.nako3(5行目): 未解決の単語があります: [単語『章選択実効』して]
1 回に 1 ファイルしか受け付けないので、check.sh はファイルごとに呼びます。
lint が素通りさせるもの¶
第 1〜3 章と、この章で見つけた「lint を通るのに誤っている」書き方を並べます。
| 誤り | lint | 実行したとき | 見つけた場所 |
|---|---|---|---|
かっこの中の未定義の関数名 (「a」を未定義命令) |
通る | エラーにならず undefined |
第 1 章 |
値の位置の未定義の名前 第1章実効を継続表示 |
通る | エラーにならず undefined を表示(終了コード 0) |
第 1 章 |
名前を 終了 にする |
通る | プロセスが終了コード 0 で終わる | 第 3 章 |
の間 の条件をかっこで囲まない |
通る | 「『while』はまだ実行に対応していません」 | 第 3 章 |
| 命令の引数が足りない | 通る | 足りない引数が別の値で埋められる | この章 |
最後の 1 つは、この章で関数の網羅の検査を作っている最中に踏みました。
引数が足りないと、別の値で埋められる¶
文字列の中から別の文字列を探す 文字検索 の書式は「AでBからCを文字検索」で、B は何文字目から探すかです。最初は「から」を省いて書きました。
(残りで名前を文字検索) > 0で戻る
テストのいくつかは通り、いくつかは落ちました。調べていくと、同じ式が、呼ぶ場所によって違う値を返していました。
他名=孤立 他長=2 名前=入口 名長=2 残り=入口を試す
最終 残り=入口を試す 名前=入口 位置=0
結果=true
位置 = 残りで名前を文字検索 は 0(見つからない)なのに、同じ式を > 0 で比べた結果は真です。足りない 1 つの引数が、その直前に計算した別の値で埋められていたようで、直前に何を計算したかで結果が変わります。lint は何も言いません。
「から」を書けば、どこで呼んでも同じ値になります。
# 文字検索 の書式は「AでBからCを文字検索」。「から」を省くと、足りない引数が直前に
# 計算した別の値で埋められ、呼ぶ場所によって結果が変わる(lint は捕まえない)。
(残りで1から名前を文字検索) > 0で戻る
同じことが起きていないか、ほかに使っている複数の引数を取る命令(配列検索・区切る・置換・正規表現置換・配列結合・最大値 など)の書式を gonako doc で 1 つずつ確かめ、単独で呼んでも正しい値を返すことを確かめました。助詞が説明と違う書き方(区切る を「の」でなく「を」で呼ぶなど)は受け付けられていましたが、引数の数が足りないものは他にありませんでした。
型の検査も無い¶
Haskell 版では GHC の -Wall -Werror が型と網羅性を、hlint が書き方を見ました。なでしこ3 版には、その両方がありません。
| 言語版 | 型の検査 | 書き方の検査 | 名前の検査 |
|---|---|---|---|
| Haskell 版 | GHC(エラーにする) | hlint | GHC |
| PHP 版 | PHPStan(レベル 9) | PHP-CS-Fixer | PHPStan |
| なでしこ3 版 | 無し | 無し(整形だけ) | gonako lint(半分だけ) |
第 1 章で見たとおり、!厳チェック という指定も、nadesiko3go 3.8.8 では検査に使われていません。残る守りはテストだけです。 だからこそ、次の 2 つを検査に加えました。
- テストファイルが最後まで走ったか(第 3 章の
終了の落とし穴への対策) - テストがソースのすべての関数に届いているか(この章の関数の網羅)
5.5 カバレッジの代わり — 関数の網羅¶
仕組みが無い¶
カバレッジは、テストがコードのどこを通ったかの割合です。第 3 波の言語版と並べると、なでしこ3 版だけが「計測」もできません。
| 言語版 | 計測の仕組み | しきい値の機能 |
|---|---|---|
| Elixir 版 | 標準 | ある |
| PHP 版 | 拡張が要る(pcov) | 無い(自作) |
| Haskell 版 | 標準(HPC) | 無い(自作) |
| なでしこ3 版 | 無い | 無い |
処理系に手を入れて実行の記録を取る方法もありますが、それは処理系の改造です。この版では、ソースを読んで分かることだけで、代わりになる検査を作ります。
テストから届く関数を数える¶
考え方は次のとおりです。
src/のファイルから関数の定義(●(…)名前とは〜 行頭のここまで)を集めるtest/のファイルの中に名前が現れる関数を「直接」届くと数える- 届いた関数の本文の中に名前が現れる関数を「間接」に届くと数え、増えなくなるまで繰り返す
- どこからも届かない関数を「未到達」として報告し、1 つでもあれば失敗する
実行しない静的な検査なので、行や分岐を通ったかどうかは分かりません。代わりに、テストの無い関数と、どこからも呼ばれない関数は確実に見つかります。関数の単位の、粗い網羅です。
この検査をなでしこ3 で書きました(tools/function_coverage_lib.nako3)。テストから書きます。
定義一覧 = [{"名前": 「入口」, "本文": 「 下請けを呼ぶ」}, {"名前": 「下請け」, "本文": 「 1で戻る」}, {"名前": 「孤立」, "本文": 「 2で戻る」}]
到達結果 = 定義一覧と「入口を試す」の到達関数算出
「テストから直接呼ばれる関数」で(到達結果["直接"])と[「入口」]が一致確認
「呼ばれた関数の本文から間接に届く関数」で(到達結果["間接"])と[「下請け」]が一致確認
「どこからも届かない関数」で(到達結果["未到達"])と[「孤立」]が一致確認
短い名前が長い名前の一部として現れる¶
名前が現れるかを文字列の検索で見るので、1 つ落とし穴があります。第 1 章の 正解率 は、第 3 章の 決定木正解率 の一部です。テストが 決定木正解率 しか呼んでいなくても、正解率 も「現れる」と数えてしまいます。
そこで、探す名前を含むより長い名前を、先に文章から取り除いてから探します。
●(文章と名前を名前一覧で)名前出現判定とは
残り = 文章
Iを 0から (名前一覧の要素数) - 1まで繰り返す
他名 = 名前一覧[I]
もし、(他名の文字数) > (名前の文字数)ならば
もし、(他名で1から名前を文字検索) > 0ならば
残り = 残りの他名を「」に置換
ここまで
ここまで
ここまで
(残りで1から名前を文字検索) > 0で戻る
ここまで
名前一覧 = [「正解率」, 「決定木正解率」]
「長い名前の一部としてだけ現れる短い名前は出現と見なさない」で(「木で予測と正解の決定木正解率」と「正解率」を名前一覧で名前出現判定)と偽が一致確認
「短い名前が単独でも現れれば出現と見なす」で(「決定木正解率と正解率」と「正解率」を名前一覧で名前出現判定)と真が一致確認
5.4 節の「引数が足りない」誤りを踏んだのは、この関数でした。
関数の見出しを取り出す¶
関数の名前は、見出しの行から ●(…) と末尾の とは を正規表現で消して取り出します。正規表現のかっこで部分を取り出す(抽出文字列)方法を最初に試しましたが、空しか返らなかったので、消す形にしました。
関数は行頭のここまで で閉じます。字下げした ここまで は もし や繰り返しの終わりです。注釈(# で始まる行)は本文に含めません。注釈に書いた関数名で「届いた」と数えないためです。
実行する¶
./bin/gonako tools/function_coverage.nako3
関数の網羅: 48 件中 48 件(直接 38 件、間接 10 件)
0.09 秒で終わります。わざとテストの無い関数を足すと、名前を挙げて落ちます。
関数の網羅: 49 件中 48 件(直接 38 件、間接 10 件)
未到達: 百分率表示
[実行時エラー]tools/function_coverage.nako3(23行目): テストから届かない関数が 1 件あります
数字の読み方——「届く」と「通る」は違う¶
最初に測ったとき、間接に届く関数は 12 件でした。その中に 前処理報告作成(第 2 章)と 決定木報告作成(第 3 章)がありました。
どちらも、テストの中では学習データがあるときだけ走る分岐からしか呼ばれていません。データの無い環境では、一度も実行されていないのです。それでも、名前が現れるので「届く」と数えられます。Haskell 版では、データが無いとカバレッジが 11 ポイント下がることで、この穴が数字に出ました。静的に数える方式では出ません。
そこで、この 2 つの関数を自作の小さなデータ(10 行)で直接呼ぶテストを足しました。期待値は、第 2・3 章と同じく Python で書いた参照実装で求めています。
小アヤメ = 「がく片長さ,がく片幅,花弁長さ,花弁幅,種類{改行}0.2,0.6,0.1,0.1,Iris-setosa{改行}…」
「自作のデータで前処理の結果を報告する」で(小アヤメを前処理報告作成)と「データ件数: 10{改行}欠損値の数: がく片長さ=1, がく片幅=0, 花弁長さ=0, 花弁幅=1, 種類=0{改行}…」が一致確認
このデータには、第 2 章の「0 = 「」 が真になる」誤りを捕まえるため、値が 0 のセルを 1 つ入れてあります(がく片幅の欠損値が 0 件と数えられることを確かめる)。
足したあとは、直接 38 件・間接 10 件になりました。「届く」は「通る」の必要条件でしかありません。 数字が 100% でも、間接に届くと数えられた関数が本当に実行されているかは、どの分岐から呼ばれているかを見て確かめます。
5.6 5 つの検査を 1 つにまとめる¶
5 つの検査を tools/check.sh にまとめています。
./tools/check.sh
== 文法(gonako lint)
== 整形(gonako format との差分)
== テスト
-- test/chapter01_test.nako3
保留: ルールによる判定の正解率がほかの言語版と一致する(学習データがありません)
検査 14 件、失敗 0 件、保留 1 件
…
-- test/random_test.nako3
検査 10 件、失敗 0 件、保留 0 件
== 関数の網羅(tools/function_coverage.nako3)
関数の網羅: 48 件中 48 件(直接 38 件、間接 10 件)
すべての検査が通りました
テストは 10 ファイル・111 件(保留 3 件)で、全体で 4 秒ほどです。関数の網羅の検査そのもの(tools/ のなでしこ3 のファイル)も、lint と整形の対象に入れています。検査の道具も、検査される側のコードと同じ規則で書きます。
第 6 章で、この check.sh を Gulp と CI から呼びます。
5.7 まとめ¶
- 依存が 1 つも無い — CSV・JSON・正規表現は処理系の命令、乱数と線形代数と機械学習は自作。管理する版は処理系と Go のツールチェーンの 2 つだけ
- 版は疑似版と
GOTOOLCHAINで固定する — タグがv無しなので疑似版を書く。Go の既定の設定ではチェックサムのデータベースで中身を確かめるので、ロックファイルが無くても「同じ版なら同じ中身」になる - 取り込みは
./を付ける — 関数名はファイルをまたいで一意にする - 整形の検査は
diffで作る —gonako formatに検査用のモードが無い。終了コードを決めているのはdiff - lint が素通りさせる誤りが 5 つある — かっこの中の未定義の名前、値の位置の未定義の名前、名前を
終了にする、の間の条件のかっこ忘れ、命令の引数の不足。引数が足りないと、直前に計算した別の値で埋められ、呼ぶ場所によって結果が変わる - 型の検査も書き方の検査も無い — 残る守りはテストだけなので、テストが最後まで走ったことと、テストがすべての関数に届いていることを検査に加えた
- カバレッジの代わりに関数の網羅を自作した — テストから直接・間接に名前が現れる関数を数える。長い名前の一部として現れる短い名前は数えない。「届く」は「通る」の必要条件でしかないので、データのあるときだけ走る分岐からしか呼ばれない関数には、自作のデータで直接呼ぶテストを足した
- 終了コードはすべて 1 — 道具が決めた値は
gonako lintだけで、ほかはdiffと自分で決めた値
次の章では、check.sh を Gulp と CI に載せます。check.sh がまとめ役になるので、検査の並びを書く場所が 1 箇所で済むことと、それと引き換えに CI の画面で失うものを扱います。
なでしこ3 版のほかの章は なでしこ3 版のトップ から辿れます。