モノイドとモナドを分けるのは継続を持てるかどうかである
IaCの成果物を語るときにモノイドとモナドという言葉を使っているが、ここで効いているのは圏論の一般論ではなく、片方は「次に何をするか」を関数の中に隠せて、もう片方は隠せないという一点だけである。この一点が、planをレビューできるかどうかを決めている。
二つの構造
モノイドは「くっつけられる」構造である。要素の集合と、二つを繋ぐ演算と、何も足さない単位元があり、繋ぎ方の順序が結果を変えない(結合律)。リストの連結が典型で、[a] ++ [b] ++ [c] をどう括っても [a,b,c] になる。
モナドは「値を受けて次の計算を返す関数を繋げられる」構造である。bind(Promiseのthen、Output.apply)が中心で、渡すのは値ではなく関数である。定義そのものはモナドは箱ではなく合成戦略であるに書いた。
>>=の右が関数なので、適用してみるまで何が出るか分からない。見えるかどうかの差はここから出る
繋ぐものが値なら、繋いだ結果を開いて全要素を数えられる。繋ぐものが関数なら、関数を適用してみるまで中身が分からない。関数の本体を覗く手段は一般には無い(それができるならホスト言語の静的解析器を書いたことになる)。
だから「planを表示してユーザーが承認する」というワークフローは、成果物がモノイドであることに依存している。モナドだと、表示できるのは「ここまで分かっている部分と、これから走る関数」でしかなく、表示されたものが実行されるものの全部だと言えない。
モナドは弱いのではなく強すぎる
順序を間違えやすいので書いておく。モナドはモノイドの上位互換ではないし、劣った構造でもない。表現力が高いことがそのまま検査可能性の喪失になっている、という取引である。
得るもの
Output.applyが書けるからこそ「デプロイ中に判明したARNを見て分岐する」が書ける。実行時にしか分からない値に応じて構造を変えられる。
失うもの
書けるからこそpreviewが原理的に近似になる。同じ性質の裏表であって、実装を頑張れば両方取れるという種類の話ではない。
Pulumiのドキュメントが「applyの中でリソースを作るのは避けよ」と禁止ではなく非推奨で書いているのは、この構造の帰結である — 禁止できるなら言語が禁止している。
「モノイドだから安全」ではない
モノイド性が保証するのは「成果物に継続が残らない」ことだけで、その成果物が正しいとも、成果物を作る過程が行儀よいとも言っていない。
- CDKの成果物(CFNテンプレート)はモノイドだが、それを組み立てるのはチューリング完全な任意プログラムであり、生成の停止性も再現性も言語の保証にならない
- モノイドの1要素として不透明な箱を埋め込むことはできる。Lambda-backedカスタムリソースがそれで、列としては平らなまま、その要素の中で任意コードが走る
- 実行時に成果物の外から効果が足されることも防げない。CFNの自動ロールバックがそれに当たる
つまりモノイド性は静的側の必要条件であって、十分条件ではない。「成果物が全情報を含む」ためには、モノイド性に加えて、生成が全域的であること、実行が成果物に閉じていることが要る。三つは別々の軸である。
自由構造という言い方
PlanがVec<Effect>であるように、効果を値として並べて意味を後から与える設計を自由モノイド + 解釈器と呼んでいる。同じ発想を継続込みでやるとfree monadになる。Carinaのplanが前者に留まるのは、効果の結果に依存して次を決める部分を静的段階で済ませてあるからで、これは制約ではなく選択である。詳細と留保は効果の具体化 — planは効果の自由モノイドである。