はじめに
こんにちは。細々とプログラミングをしているsotanengelです。
最近、生成AIに Python のコードを書かせることが増えてきたのですが、動くコードはそこそこ出てくる一方で、
- 関数の責務が重い
- I/O や副作用が関数の中に混ざる
- 再代入が多くて、関数型っぽく整理されていない
みたいなコードもそれなりに出てくるなぁと思っていました。
もちろん formatter や linter は便利なのですが、もう少し
- 関数単位で見たい
- 「関数型プログラミングのルールに反していないか」を見たい
- ついでに JSON で機械可読に出したい
みたいな気持ちがあったので、今回 Rust 製の Python 関数型ルールチェッカー を実際に作ってみました。
その頃に、以下の本を読んで「こういう観点を Python のコードに対して機械的にチェックできるライブラリを作れたら面白そうだな」と思ったのも、今回の実装のきっかけです。
ざっくり言うと、Python コードを解析して、関数ごとに関数型プログラミングのルール違反を検出・報告するツール です。Rust でコアを実装し、PyO3 + maturin で Python パッケージとしても扱える形にしました。
以下、どういうものを作ったのかや、実装方針などをざっくりまとめておきます。
使い方
一応、README や docs にも説明は書いているのですが、こちらでもざっくり書いておきます。
※ まだ今後の改善余地はあるため、仕様や出力形式が今後少し変わる可能性があります。問題があれば issue や PR などで教えていただけると助かります。
1. どんなツールなのか
今回作成したツールは、以下のようなことができます。
- Python ソースコードを解析する
- 関数単位で「関数型プログラミングのルール違反」を検査する
- デフォルトではすべての関数を検査対象にする
- 明示的な除外マーカーが付いた関数は検査対象外にする
実装としては、
- 解析コアは Rust
- Python への公開は PyO3 + maturin
- Python 構文解析は parser adapter 層経由
- ルールエンジンは拡張可能な設計
- 出力は人間向けと機械向けの両対応
- ローカル開発・CI・将来の AI 自動修正を見据えて JSON 出力にも対応
という形にしています。
2. どうやって使うのか
CLI としては、たとえば以下のような形で使えます。
fp-checker check path/to/project
単一ファイルにもディレクトリにも対応しており、text / JSON の両方で結果を出せるようにしています。
3. 実際にどんな出力が出るのか
文章だけだと分かりにくいと思うので、手元で FastAPI のサンプル API に対して実行したときの内容も貼っておきます。
まず、fp-checker 0.1.3 を入れた状態で fp-checker check app を実行したところ、確認できた警告は以下の 2 つでした。
-
index関数内の代入が多すぎる -
index関数がグローバルなWEATHER_CODESを参照している
実行イメージとしては、だいたいこんな感じです。
$ fp-checker --version
fp-checker 0.1.3
$ fp-checker check app
warning: too many assignments in function 'index'
warning: function 'index' reads global 'WEATHER_CODES'
この結果を見て、
- 天気コード変換
- 表示用データ整形
- API 呼び出しとエラー処理
あたりを index から分離しました。
その後に再度チェックしたところ、警告は 0 になりました。
$ fp-checker check .
No issues found.
加えて、そのままアプリ側も確認していて、以下の流れでローカル検証しています。
$ python -m compileall app
$ uvicorn app.main:app --port 8001
$ open http://127.0.0.1:8001/?city=Tokyo
最終的には、
-
fp-checkerの警告は0 - アプリは正常に起動
- 東京の天気表示を確認
というところまで確認できました。
4. 除外したい関数はどうするか
「全部の関数を検査対象にする」とはいっても、例外的に除外したい関数もあるはずなので、そのための ignore 方式も入れました。
@fp_ignore
def f():
...
また、JSON / text の両出力で除外関数数も出せるようにしています。
5. 実装したルール
初期の最小ルールセットとしては、以下を MVP の必須ルールとして実装しました。
- 可変グローバル状態の参照・更新
- 関数内での破壊的代入の多用
- I/O の直接実行
- 時刻・乱数など非決定的要素の直接使用
- 外部状態への副作用
- ミュータブルなデフォルト引数
-
print/logging/open/requests等の直接呼び出し -
global/nonlocalの使用
このあたりを個別ルールとして実装し、1 関数に対して複数の diagnostic を出せるようにしました。
6. 出力形式
結果の出力としては、
- text 出力
- JSON 出力
- exit code 制御
- 違反なし: 0
- 違反あり: 1
- 実行エラー: 2
- 概要統計
- 対象ファイル数
- 対象関数数
- 除外関数数
- ルール違反数
を出せるようにしました。
人間にも AI にも扱いやすい形にしたかったので、JSON 出力はかなり重視しています。
また、関数単位 assessment をレポートにも載せるようにしていて、function_assessments を report に追加し、text 出力では purity / style を表示、JSON schema と SARIF にも反映しています。fail_on も diagnostic だけでなく assessment severity を見るようにしました。
そのため、単に「違反があるか」だけでなく、「どの関数が purity 的に厳しいのか / style 的に厳しいのか」まで CI で扱いやすくなっています。
JSON 側は例えばこんな雰囲気です。
{
"summary": {
"files": 1,
"functions": 3,
"ignored_functions": 0,
"violations": 2
},
"diagnostics": [
{
"function": "index",
"rule_id": "too_many_assignments",
"severity": "warning"
},
{
"function": "index",
"rule_id": "global_state_read",
"severity": "warning"
}
],
"function_assessments": [
{
"function": "index",
"purity": "impure",
"style": "imperative"
}
]
}
さいごに
もしも今回の記事を読んで、
- こういうルールも欲しい
- JSON schema をこうした方が使いやすい
みたいなものがあれば、issue や PR などで教えていただけるとありがたいです。
