決定論的なコア、非決定論的なシェル
Deterministic Core, Non-Deterministic Shell (outdata.net)
要約
本記事は、ソフトウェア設計における「決定論的なコア」と「非決定論的なシェル」という概念について考察しています。予測可能で一貫した動作をするコア部分と、外部要因やユーザーインタラクションによって変化する柔軟なシェル部分を分離することで、システムの堅牢性と適応性を両立させるアプローチを探求しています。
全文翻訳
決定論的なコア、非決定論的なシェル
2026年8月3日
14年前、Gary Bernhardtは「Functional Core, Imperative Shell」という言葉を生み出しました。コンピューティングにおける多くの優れたアイデアと同様に、それは全く新しいものではありませんでしたが、彼の概念は非常に明快であり、既存のシステムにおけるテストと決定論について議論するための優れた基盤を形成しています。
簡単に言うと、Functional Core/Imperative Shellアーキテクチャは、コードを2つの部分に分割します。Functional Coreは純粋関数型です。つまり、IOがなく、破壊的な状態更新もありません。アプリケーションのビジネスロジックを担当します。Imperative Shellは比較的パスが少なく、状態を維持し、外部依存関係を調整し、外部世界(つまりIO)を扱います。その仕事は、コアに値をクエリし、ブラックボックスの決定の結果として値を受け取り、それを使って外部世界と対話することです。データベースへの書き込み、リクエストの送信、GUIの更新などです。
このモデルにおけるシェルとコアは、明確な特性を持っています。
| コア | シェル |
|---|---|
| 決定を行う | 依存関係を調整する |
| 多くの分岐実行パス | より線形な実行 |
| 世界から隔離されている | 世界と統合する |
これにより、コアはテストに非常に適しています。純粋関数型であるため、同じ入力は常に同じ結果をもたらします。隔離されているため、モックやスタブするものがありません。そして、複雑なビジネスロジックを扱うため、テストはシステムの動作について多くのことを教えてくれます。
関数型純粋性と決定論
純粋関数がテストに適している特性をより短く説明する言葉は、それらが決定的であるということです。つまり、入力ストリームが与えられた場合、純粋関数は常に同じ出力ストリームを返します。その振る舞いは再現可能です。しかし、純粋関数型プログラミングだけがその方法ではありません。少し首を傾げると、値のストリームと代入のシーケンスが同じものを表現する異なる方法であることがわかりますし、状態機械も同様の利点をもたらします。以下のコードを考えてみてください。
function add(ns) {
return ns.reduce((a, b) => a + b, 0)
}
class AddMachine {
#state = 0
transition(input) {
this.#state += input
}
get state() {
return this.#state
}
}
add関数は理解しやすく、純粋であり、したがって決定的です。しかし、AddMachineも決定的です。transition関数への呼び出しシーケンスが同じであれば、AddMachineは同じ状態を返します。命令型であることは、それを変えません。
const output = add([1, 2, 3])
const a = new AddMachine()
a.transition(1)
a.transition(2)
a.transition(3)
const output = a.state
純粋関数型プログラミングは素晴らしいパラダイムですが、言語やパフォーマンスの考慮事項から、常に実用的とは限りません。C言語で試したいとは思いません!しかし、要件を純粋関数型から単に決定的に緩和することで、「Functional Core, Imperative Shell」のテスト容易性の利点を維持しつつ、その適用範囲を広げることができます。そして、この投稿のタイトルにつながります:決定論的なコア、非決定論的なシェル。
決定論は、関数型純粋性よりも抽象的な概念のように感じられるかもしれません。どのように認識すればよいのでしょうか?非決定的なものから始めて逆算する方が簡単だと私は思います。以下に、非再現可能な振る舞いの一般的な例をいくつか示します。
- シードされていないRNG(乱数生成器)の呼び出し
- 非同期およびマルチスレッド操作
- ネットワーク経由の通信
- 他のプロセスとの通信
- ローカルストレージへの読み書き
- データベースとのやり取り
- OSに日付や時刻を問い合わせる
これらはすべて非決定論的なシェルに属します。ビジネスロジックの中でこれらを見つけるたびに、デフラグメンテーションの自然なターゲットがあります。つまり、それらの周りで関数を2つに分割するか、それらを一層上に持ち上げて結果をパラメータとして注入します。「シェル」の比喩を文字通りに考えるのは有益です。それはロジックを囲み、アプリケーションの核に問い合わせて必要なものを取得するはずです。
手持ちのものを活用する
「これはすべて結構なことだが」とあなたは思うかもしれません。「業界のレガシー&バイブコードの鉱山で働いている私にとって、どんな役に立つのだろうか?」公平な非難です、想像上の読者よ。誰もがFoundation DBのように最初からその区別をつけられるわけではありません(実際にはさらに一歩進んでいますが、それは別の投稿のトピックです)。私がこれまで見てきたほとんどすべての実世界のコードベースでは、決定論と非決定論は密接に関連しており、私はかなりの数を見てきました。
しかし、完璧を善の敵にしてはいけません!平均的な(つまり、ひどい)コードベースを考える一つの方法は、多くの決定論的なコアを持っているということです。空の星空のように、それらはスロップの中に何千も散らばっています。ガラス半分空の視点は、これらのコードベースが救いようのないレガシーの混乱であるということです。しかし、ガラス半分満タンの視点は、どこかに多くの決定論的なコアが隠されているということであり、おそらくほんの一握りです。
古いWindowsシステムユーザーは「ディスクデフラグメンター」を覚えているかもしれません。それは、スピニングハードディスク全体に物理的に散らばっていたファイルの内容を、連続したものにしました。メディア上の物理的な距離に読み取り速度が依存していた時代には、これは非常に重要でした。
[画像: Windows XPのディスクデフラグメンターツール]
赤いセグメントがゆっくりと青に変わっていくのを見るのは、非常に満足感がありました。
既存のコードに対する段階的なアプローチの一つは、決定論のデフラグメンテーションを実践することです。ファイル、クラス、あるいは個々の関数の数行であっても、どこでもそれを見つけて収集し始めてください。グループ化できる決定論が増えれば増えるほど、テストしやすい機能が増え、プログラム全体の動作と信頼性についてより自信を持つことができます。「テストしにくい」(非決定論的なコード)の表面積が縮小し始めます。十分に大きなコードベースでは、単一の決定論的なコアに到達することは決してないでしょうが、数千よりも数百でも良いのです。
内なる状態機械を解き放て!
私がこれまで見てきたひどいコードベースの山には、その中に閉じ込められた、より優れた決定論的な状態機械が一つ以上あります。それが明らかでなくても、それらがそこにあることをお約束します。そしてそれらを見つけたら、ソフトウェアの変更とテストがいかに簡単になるかに喜ぶでしょう。少しずつ、信頼性を築き上げることができます。