<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>純粋関数</title><link>https://blog.ast.moe/tags/%E7%B4%94%E7%B2%8B%E9%96%A2%E6%95%B0/</link><description>Recent posts from 五里霧中</description><generator>Hugo</generator><item><title>Goにおける明示的な純粋性制約の静的検査: mikotoの設計と検証</title><link>https://blog.ast.moe/blog/2026-09-30-2/</link><pubDate>Wed, 30 Sep 2026 22:38:00 +0900</pubDate><guid>https://blog.ast.moe/blog/2026-09-30-2/</guid><description>&lt;h2 id="概要"&gt;概要&lt;/h2&gt;
&lt;p&gt;Goの関数の長さと、明示的に指定した関数の純粋性に関する制約を検査する&lt;a href="https://github.com/ieee0824/mikoto"&gt;mikoto&lt;/a&gt;を作った。関数宣言へ&lt;code&gt;//mikoto:pure&lt;/code&gt;を付けると、値型だけを用いた入出力、パッケージ変数へのアクセス、参照先の変更、ポインタの間接参照、呼び出し先などを検査する。実装にはGoのASTと型情報を使い、検査対象を同一パッケージ内の関数宣言に限定している。&lt;/p&gt;
&lt;p&gt;背景にあるのは、コードの量が増えたとき、設計上の意図を毎回人間が読み直して確認することの難しさだ。「この関数は入力から結果を計算するだけ」という意図があっても、後から時刻の参照や共有状態の更新が加われば、その前提は変わる。そこで、守りたい制約をソースへ明示し、実装がその制約から外れたときに診断する形にした。&lt;/p&gt;
&lt;p&gt;初期実装を検証すると、型パラメータを介したmapの走査や、配列ポインタの暗黙の間接参照を見逃す一方、正当なジェネリック呼び出しや括弧付き操作を拒否する問題が見つかった。&lt;a href="https://github.com/ieee0824/mikoto/pull/6"&gt;PR #6&lt;/a&gt;ではこれらと行数計算の問題を修正した。この記事では、検査モデル、実装、修正例、検証結果を順に示し、診断がないことから何を言えて、何を言えないかを考える。&lt;/p&gt;
&lt;p&gt;以下は学術論文のように問題設定と評価を分けた開発記録であり、一般的なGoプログラムの参照透過性を証明した報告ではない。対象は2026年9月30日の&lt;code&gt;main&lt;/code&gt;、&lt;a href="https://github.com/ieee0824/mikoto/tree/2235f226aa8cb7e88e6a1e0e3768e2202cdaba97"&gt;commit &lt;code&gt;2235f226aa&lt;/code&gt;&lt;/a&gt;である。提案する使い方と、実装で確認できる動作も区別して書く。&lt;/p&gt;
&lt;h2 id="1-背景と問題設定"&gt;1. 背景と問題設定&lt;/h2&gt;
&lt;h3 id="11-実装量と確認量は同じ速度で増えない"&gt;1.1 実装量と確認量は同じ速度で増えない&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://blog.ast.moe/blog/2026-09-25-2/"&gt;AI Codingとコードレビューについての記事&lt;/a&gt;では、人間がすべてのコードを同じ密度で読み続けることに依存せず、仕様、テスト、静的解析、観測性を強くするという考え方を書いた。mikotoは、そのうち機械的に確認できる設計上の制約を、小さい範囲から検査する試みになる。&lt;/p&gt;
&lt;p&gt;コードレビューでは、個々の式の正しさだけでなく、関数の役割が変わっていないかも確認する。料金を計算する関数がデータベースを更新していないか。入力を分類する関数が現在時刻を直接読んでいないか。小さな計算のための関数に、通知、ログ出力、共有キャッシュの更新が追加されていないか。どれもコンパイラの型検査だけでは、設計上の問題としては扱われない。&lt;/p&gt;
&lt;p&gt;こうした変更は必ずしもバグではない。時刻を読むことも、状態を更新することも、システム全体では必要だ。ただ、入力から結果を計算する部分と、外界を読み書きする部分を分けていたなら、その境界を変える判断が必要になる。コードが動くことと、意図していた責務に収まることは、別々に確かめたい。&lt;/p&gt;
&lt;p&gt;そこで「副作用がありそうか」を都度LLMへ尋ねる前に、明確に禁止したい構文や依存先を解析ツールで調べることを考えた。診断の根拠がソース上の操作に対応していれば、変更した人は、その操作を計算部分から外すのか、関数の責務を変えるのかを判断できる。問題を見つける仕組みと、その問題をどう扱うか決める仕事を分けられる。&lt;/p&gt;
&lt;h3 id="12-二つの制約を一つのツールで扱う"&gt;1.2 二つの制約を一つのツールで扱う&lt;/h3&gt;
&lt;p&gt;mikotoには、関数の長さの検査と、指定した関数の純粋性制約の検査がある。前者はすべての対象関数に適用し、後者はマーカーを付けた関数だけに適用する。長さの上限は既定で80行、&lt;code&gt;-max-lines=0&lt;/code&gt;を指定すると行数検査を無効にできる。&lt;/p&gt;
&lt;p&gt;両者は関連するが、同じ性質ではない。短い関数でも外部状態を複雑に変更できるし、長い関数でも値の変換だけを行える。関数を短く分割しただけで、依存先が明確になるとも限らない。行数は読む範囲を見直すための指標、純粋性制約は外部状態との関係を制限するルールとして、独立に扱っている。&lt;/p&gt;
&lt;p&gt;一つの関数を何行までにするかは、プロジェクトの方針で変わる。80行という既定値に品質の普遍的な境界があるわけではない。同様に、すべての関数を純粋にすることも目的ではない。ファイルを開く関数、HTTP要求を送る関数、トランザクションを開始する関数には、外界とのやり取りが本来の責務としてある。&lt;/p&gt;
&lt;p&gt;この記事で扱う問いは、「Goの任意の関数が純粋かを完全に判定できるか」より狭い。入力に対する計算へ役割を限定したい関数を明示したとき、その境界を破る操作を、どの程度小さく、説明可能なルールで検査できるか。そして、そのルールをGoの構文と型の仕組みに合わせて実装すると、どこで単純な判定が破綻するか、という問いである。&lt;/p&gt;
&lt;h2 id="2-純粋性をどのような性質として扱うか"&gt;2. 純粋性をどのような性質として扱うか&lt;/h2&gt;
&lt;h3 id="21-外部への変更と外部への依存"&gt;2.1 外部への変更と外部への依存&lt;/h3&gt;
&lt;p&gt;純粋関数を説明するとき、「副作用がない」という言葉だけでは範囲が曖昧になる。外部状態を書き換えなくても、それを読み取って結果を変える関数は、入力だけから結果が定まらない。現在時刻を返す関数は、自分で時計を変更していなくても、同じ引数で別の値を返せる。&lt;/p&gt;
&lt;p&gt;例えば次の二つには、異なる問題がある。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rate&lt;/span&gt; = &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Multiply&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;n&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;n&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Count&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;n&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;rate&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;n&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Multiply&lt;/code&gt;はパッケージ変数の現在値に依存する。&lt;code&gt;Count&lt;/code&gt;は戻り値だけ見れば同じ入力で同じ結果になるが、呼び出すことで外部状態を変更する。戻り値の決定性と、外部状態を変えないことは、分けて考える必要がある。mikotoは、指定した関数からパッケージ変数へアクセスすることを、読み書きの両方で診断する。&lt;/p&gt;
&lt;p&gt;概念的には、引数を&lt;code&gt;x&lt;/code&gt;、呼び出し前の外部状態を&lt;code&gt;s&lt;/code&gt;、戻り値を&lt;code&gt;y&lt;/code&gt;、呼び出し後の外部状態を&lt;code&gt;s'&lt;/code&gt;として、関数の実行を次のように書ける。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;execute(f, x, s) = (y, s&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;外部状態を変えない: s&amp;#39; = s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;結果が外部状態に依存しない: y = g(x)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;これは説明のためのモデルで、mikotoが実際に状態空間を列挙して確認しているわけではない。また、何を外部状態と観測に含めるかで性質の意味が変わる。メモリ割り当てやGCまで含め、機械の状態が一切変わらないことを要求したら、普通の計算もほとんど成立しない。ここで関心があるのは、計算の結果と、呼び出し元や外界から観測する依存・変更の関係だ。&lt;/p&gt;
&lt;h3 id="22-参照透過性との距離"&gt;2.2 参照透過性との距離&lt;/h3&gt;
&lt;p&gt;参照透過性は、式をその値で置き換えても観測される振る舞いが変わらない、という考え方で説明できる。ある計算を一度行って結果を再利用することと、必要な場所で毎回実行することが同じなら、計算の置き場所や回数を考えやすくなる。&lt;/p&gt;
&lt;p&gt;ただし、プログラムでは戻り値だけが観測対象ではない。panic、無限ループ、資源消費、浮動小数点の扱い、実行環境の違いも関わる。さらに、戻り値をGoの&lt;code&gt;==&lt;/code&gt;で比較すればよいとも限らない。NaNのように、自分自身との比較が真にならない値もある。意味上の同じ結果と、特定の比較演算の結果を混同したくない。&lt;/p&gt;
&lt;p&gt;mikotoの&lt;a href="https://github.com/ieee0824/mikoto/blob/2235f226aa8cb7e88e6a1e0e3768e2202cdaba97/README.md"&gt;README&lt;/a&gt;でも、検査を参照透過性の数学的な証明とはしていない。診断がなかったという結果は、現在のルールに抵触する操作を検出しなかったという結果だ。メモ化や実行順の変更を正当化する証明書が生成されるわけではない。&lt;/p&gt;
&lt;p&gt;それでも、外部状態を読む経路と変更する経路を減らすことには意味がある。原因が入力へ寄れば、テストの条件を作りやすくなり、失敗したときに再現すべき情報も整理しやすい。普遍的な保証を得る前に、調べる範囲を小さくする効果がある。&lt;/p&gt;
&lt;h3 id="23-ローカル変数の変更は許可する"&gt;2.3 ローカル変数の変更は許可する&lt;/h3&gt;
&lt;p&gt;外部状態を変更しないことと、関数の内部で一切代入しないことは違う。例えば整数の和を計算するとき、ローカル変数を更新して結果を蓄積しても、その変数が外へ共有されていなければ、呼び出し元の状態を変更したことにはならない。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;//mikoto:pure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Sum&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;n&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;total&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;i&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;range&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;n&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;total&lt;/span&gt; &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;i&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;total&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;現在のmikotoはこの関数を受理する。&lt;code&gt;total&lt;/code&gt;と&lt;code&gt;i&lt;/code&gt;への代入があるから拒否する、というルールではない。通常の代入では左辺の括弧を外し、識別子なら許可する。もしその識別子がパッケージ変数なら、別の名前参照の検査で診断する。&lt;/p&gt;</description></item></channel></rss>