Rich HickeyのClojure的発想を平易に紹介:簡潔性、不変性、より良いデフォルトが複雑系を落ち着かせ、安全性を高める実践的教訓。言語を問わず適用できる考え方を解説します。

ソフトウェアがいきなり複雑になることは稀です。締め切りを守るための簡易キャッシュ、コピーを避けるための共有ミュータブルなオブジェクト、「これは特別だから」の例外処理──それらは一つひとつは合理的に見える決定ですが、積み重なると変更が怖くなり、バグが再現しにくくなり、新機能の追加に以前より時間がかかるようになります。
複雑さが勝つのは短期的な快適さを提供するからです。既存のものを簡素化するより新しい依存関係を組み込む方が速いことがあり、状態をパッチする方が状態が五つのサービスに散らばっている理由を問うより簡単です。システムがドキュメントより速く成長するとき、慣習や部族的知識に頼りたくなる誘惑も強くなります。
これはClojureのチュートリアルではありませんし、Clojureを知らなくても十分に価値が得られるように書いています。目的は、Rich Hickeyの議論にしばしば結びつく実践的な考え方を借りて、言語に依らず日常のエンジニアリング判断に適用できるヒントを提供することです。
ほとんどの複雑さはあなたが意図的に書いたコードではなく、ツールがデフォルトで何を簡単にしてしまうかから生まれます。デフォルトが「どこでもミュータブルなオブジェクト」なら、隠れた結合が生まれます。デフォルトが「状態はメモリにある」なら、デバッグや追跡が難しくなります。デフォルトは習慣を形作り、習慣がシステムを形作ります。
ここでは三つのテーマに焦点を当てます。
これらはドメインから複雑さを取り除くわけではありませんが、ソフトウェアがそれを増幅するのを止める手助けになります。
Rich Hickey はClojureの作成者として知られる長年のソフトウェア開発者で、一般的なプログラミング習慣に挑む数々の講演でも知られます。彼の焦点は流行を追うことではなく、システムが変えにくく、推論しにくく、成長したときに信頼しにくくなる繰り返し起きる原因を見極めることです。
ClojureはJVM(Javaのランタイム)やJavaScriptといった既存プラットフォーム上で動くモダン言語です。既存のエコシステムと協調しつつ、情報をプレーンなデータとして表現し、不変な値を好み、「何が起きたか」を「画面に表示するもの」と分けるスタイルを促します。
隠れた副作用から遠ざかるようにあなたをそっと導く言語、と考えてよいでしょう。
Clojureは短いスクリプトを短くするために作られたわけではなく、繰り返し起きるプロジェクトの痛みに焦点を当てています:
Clojureのデフォルトは可変部品を減らす方向に働きます:安定したデータ構造、明示的な更新、安全に協調するツール群。
価値は言語を乗り換えることに限定されません。Hickeyの核となる考え――不要な相互依存を取り除いて簡素化する、データを永続的な事実として扱う、可変状態を最小化する――はJava、Python、JavaScriptなどどの言語でもシステム改善に役立ちます。
Rich Hickeyは**simple(簡潔)とeasy(簡単)**を鋭く区別します──そして多くのプロジェクトは気づかないうちにこの線を越えています。
Easyは今どう感じるかに関するものです。Simpleは部品の数とそれらの絡み合いの強さに関するものです。
ソフトウェアでは「簡単」はしばしば「今日タイプするのが速い」を意味し、「シンプル」は「来月壊れにくい」を意味します。
チームはしばしば当面の摩擦を減らす近道を選びますが、それは目に見えない構造の増加という負債を生みます:
各選択は一見スピードの向上に見えますが、可動部品、特別ケース、相互依存を増やし、どんどん脆くしていきます。
迅速なデリバリーは素晴らしいことですが、簡潔化せずに速さだけ追うと将来への借金をしていることが多いです。その利息は再現しにくいバグ、オンボーディングの遅さ、変更に「慎重な調整」が必要になる形で現れます。
設計やPRをレビューするときに次を問いかけてください:
「状態」とはシステム内で変化し得るもののことです:ユーザーのカート、口座残高、現在の設定、ワークフローの進行状況など。厄介なのは変化そのものではなく、変化は常に事が食い違う機会を生むということです。
同じ情報が時間や場所によって異なり得るとき、コードは常に「今どのバージョンが本物か?」という問いに応え続けなければならず、その答えを間違えるとランダムに感じるエラーが生まれます。
ミュータビリティはオブジェクトをその場で編集することを意味します。「同じ」ものが時間とともに違うものになるということです。効率的に思えますが、推論を難しくします。なぜなら先ほど見たものを信頼できなくなるからです。
身近な例は共有スプレッドシートです。複数人が同じセルを編集できると、合計が変わったり数式が壊れたり、行が整理されて見えなくなったりします。誰かが悪意を持っているわけでなくても、共有可能で編集可能であるという性質自体が混乱を生みます。
ソフトウェアの状態も同様です。二つの部分が同じミュータブルな値を読むと、片方が静かにそれを変更している間にもう片方は古い仮定で処理を続けてしまい得ます。
ミュータブルな状態はデバッグを考古学に変えます。バグ報告が「データが10:14:03に誤って変更された」と教えてくれることは稀です。見えるのは結果だけ:間違った数値、予想外のステータス、たまに失敗するリクエスト。
状態が時間とともに変わるため、最重要の問いは「ここに至るまでにどのような編集の順序があったのか?」になります。それを再構築できなければ振る舞いは予測不可能になります:
不変性とは単純に「作成後に変わらないデータ」です。既存の情報をその場で編集する代わりに、更新を反映した新しい情報を作ります。
レシートを思い浮かべてください:印刷されたレシートを消して項目を書き直したりはしません。何かが変われば訂正レシートを出します。古いレシートは残り、新しいものは明確に“最新版”です。
データが静的であれば、陰で行われる編集を心配する必要がなくなります。これにより日常の推論がずっと楽になります:
これがHickeyが簡潔性を語る大きな理由の一つです:隠れた副作用が少なければ追うべき精神的分岐も少なくなります。
新しいバージョンを作ることは無駄に見えるかもしれませんが、その代替を比較してみてください。その場で編集すると「誰がこれを変えたのか?いつ?以前はどうだったのか?」と問わねばならなくなります。不変データなら変更は明示的です:新しいバージョンが存在し、古いものもデバッグ、監査、ロールバックのために残ります。
Clojureは更新を古いもののミューテーションではなく新しい値を生成することとして扱うことを自然にします。
不変性は無料ではありません。オブジェクトを多く割り当てる可能性があり、「これをただ更新すればいい」という習慣があるチームでは慣れるまで時間が必要です。良い点は、現代の実装は下層で構造を共有してメモリコストを減らすことが多く、見返りは典型的に落ち着いた、説明のつかない事故の少ないシステムです。
並行処理とは単に「多くのことが同時に起きること」です。多数のリクエストをさばくウェブアプリ、レシートを生成しつつ残高を更新する決済システム、バックグラウンドで同期するモバイルアプリ──これらはすべて並行的です。
問題は多くのことが同時に起きること自体ではなく、それらが同じデータに触れることです。
二つのワーカーが同じ値を読み取り後に修正し得ると、最終結果はタイミングに依存します。これが**競合条件(race condition)**です:忙しいときに現れるが再現が難しいバグです。
例:二つのリクエストが注文合計を更新しようとする。
何も“クラッシュ”はしていませんが、更新が失われました。負荷が高くなるとこうしたタイミング窓はより一般的になります。
従来の解決策――ロック、同期ブロック、注意深い順序付け――は機能しますが、協調を全員に強います。協調は高コストです:スループットを遅らせ、コードベースが成長すると脆くなります。
不変データでは値はその場で編集されません。代わりに、変更を表す新しい値が生成されます。
この単純な転換が多くの問題カテゴリを排除します:
不変性が並行処理を無償にするわけではありません—どのバージョンを現在と見なすかのルールは依然として必要です。しかしデータ自体が“動く標的”でなくなるため、負荷やジョブの滞留があってもタイミング依存の謎めいた障害が起きにくくなります。
「より良いデフォルト」とは、安全な選択が自動的に起きることで、余計なリスクを負うのは明示的にオプトアウトしたときだけ、という状態を指します。
これは小さなことに見えますが、デフォルトは月曜の朝に人が書くもの、金曜の夕方にレビュアーが受け入れるもの、新しいチームメンバーが最初に触れるコードから学ぶワーキングスタイルを静かに教えます。
「より良いデフォルト」はすべての決定を代替するわけではありません。一般的な経路を誤りにくくすることを目指します。
例えば:
これらですべての複雑さが消えるわけではありませんが、それが広がるのを防ぎます。
チームはドキュメントだけでなく、コードが「やりたがる」ことに従います。
共有状態のミューテーションが簡単だとそれが常套手段になり、レビュアーは意図を議論することになります:「ここで安全なのか?」。逆に不変性や純関数がデフォルトだとレビュアーはロジックと正当性に集中でき、リスクの高い操作が目立ちます。
言い換えれば、より良いデフォルトは健全なベースラインを作ります:ほとんどの変更が一貫して見え、例外的なパターンは質問されやすくなります。
長期的な保守は既存コードを安全に読み替え・変更することが大半です。
より良いデフォルトは新しいメンバーのランプアップを助けます(「注意して、この関数は裏であのグローバルマップを更新する」などの隠れたルールが少ない)。システムが推論しやすくなり、将来の機能や修正、リファクタのコストが下がります。
Hickeyの講演で有用な思考の一つは、**事実(起きたこと)とビュー(現在私たちが信じていること)**を分離することです。多くのシステムは最新値だけを保存して過去を上書きしてしまい、時間の概念を失わせます。
事実は不変の記録です:「注文#4821が10:14に行われた」「支払いが成功した」「住所が変更された」。これらは編集されず、現実が変わるたびに新しい事実を追加します。
ビューはアプリが今必要とするものです:「現在の配送先は?」や「顧客の残高は?」。ビューは事実から再計算され、キャッシュやインデックス、マテリアライズドビューにして高速化できます。
事実を保持すると次が得られます:
レコードを上書きするのはスプレッドシートのセルを更新するようなもの:最新の数値しか見えません。
追記専用ログは記帳のようなもの:各エントリが事実であり、「現在残高」はエントリから計算されたビューです。
フルなイベントソーシングに移行する必要はありません。多くのチームは小さく始めます:重要な変更のための追記専用の監査テーブル、一部の高リスクワークフローの不変な変更イベント、スナップショット+短期間の履歴ウィンドウなど。肝心なのは習慣:事実を耐久的に扱い、現在の状態を便宜的な射影と見ることです。
Hickeyの実践的なアイデアの一つはデータファーストです:システムの情報をプレーンな値(事実)として扱い、振る舞いはその値に対して実行するものとする。
データは耐久的です。明確で自己完結した情報を保存すれば、後で再解釈したり、サービス間で移動したり、再インデックスしたり、監査したり、新機能に流し込んだりできます。振る舞いは耐久性が低い—コードは変わる、仮定は変わる、依存は変わる。
これらを混ぜるとシステムは粘着性を持ちます:データを再利用するために振る舞いも引きずらねばならなくなります。
事実とアクションを分離すると結合度が下がり、コンポーネントはデータ形状に合意するだけでよく、共有の実行経路に依存しなくてよくなります。
レポーティング、サポートツール、請求サービスは同じ注文データを消費し、それぞれ独自のロジックを適用できます。ストレージにロジックを埋め込むと、すべての消費者がそのロジックに依存し、変更が危険になります。
クリーンなデータ(進化しやすい):
{
"type": "discount",
"code": "WELCOME10",
"percent": 10,
"valid_until": "2026-01-31"
}
ストレージ内にミニプログラムを埋め込む(進化しにくい):
{
"type": "discount",
"rule": "if (customer.orders == 0) return total * 0.9; else return total;"
}
後者は柔軟に見えますが、データ層に複雑さを押し付けます:安全な評価器、バージョニングルール、セキュリティ境界、デバッグツール、ルール言語が変わる際の移行計画が必要になります。
保存された情報がシンプルで明示的なら、時間とともに振る舞いを変えても過去の記録を読み続けられます。新しいサービスは古い実行ルールを“理解する”必要がなく、異なる解釈を導入するのは新しいコードを書くことで済み、履歴を書き換える必要がありません。
大抵のエンタープライズシステムは一つのモジュールのせいで壊れるのではなく、すべてが互いに結びつきすぎることで失敗します。
密結合は「小さな」変更が何週間もの再テストを引き起こす形で表れます。あるサービスにフィールドを追加したら下流の3つを壊す。共有データベーススキーマが調整のボトルネックになる。単一のミュータブルキャッシュやシングルトンの設定オブジェクトがコードベースの半分の依存になっている。
多くの部分が同じ変わるものを共有すると、連鎖的な変更が自然に発生します。チームはより多くのプロセス、より多くのルール、より多くのハンドオフで対応し、しばしばデリバリをさらに遅くします。
言語を変えたり全書き換えをせずにHickeyの考えを適用できます:
データが足元で変わらなければ「どうしてこの状態になった?」と悩む時間が減り、コードの振る舞いについて考える時間が増えます。
デフォルトが不一致の温床になります:各チームが独自のタイムスタンプ形式、エラー形状、リトライ方針、並行性アプローチを発明します。
より良いデフォルトはバージョン管理されたイベントスキーマ、標準の不変DTO、明確な書き込みの所有権、シリアライズ/検証/トレーシング用の承認済み少数ライブラリのようなものです。その結果は驚きの少ない連携とワンオフ修正の減少です。
変化が既に起きているところから始めます:
このアプローチはシステムを動かし続けながら信頼性とチームの調整を改善し、スコープを小さくして終わらせることができます。
これらの考えを適用しやすくするのは、ワークフローが迅速で低リスクな反復をサポートする場合です。例えば、チャットベースのvibe-codingプラットフォームであるKoder.aiで新機能を作ると、二つの機能が直接「より良いデフォルト」マインドセットに合います:
スタックがReact + Go + PostgreSQL(あるいはモバイルはFlutter)であってもコアの指摘は同じです:日々使うツールは静かに作業のデフォルトを教えます。トレーサビリティ、ロールバック、明示的な計画を日常にするツールを選べば、その場しのぎのパッチに頼る圧力を減らせます。
簡潔性と不変性は強力なデフォルトであって、道徳的な掟ではありません。システムが成長する際の予期せぬ変更を減らすのに役立ちますが、実際のプロジェクトには予算や締め切り、制約があり、時にはミュータビリティが適切な道具です。
パフォーマンスが重要なホットスポット(緊密なループ、高スループットの解析、グラフィックス、数値処理)ではミュータビリティが実用的です。また範囲が管理されている場合も許容されます:関数内のローカル変数、狭いインターフェースの後ろに隠されたプライベートキャッシュ、単一スレッドコンポーネントなど。
重要なのは「封じ込め」です。ミュータブルなものが外に漏れない限り、複雑さがコードベース全体に広がることはありません。
ほとんど関数型スタイルでもチームは明確な所有権を必要とします:
ここでClojureのデータ指向と明示的境界へのバイアスは助けになりますが、この規律は言語固有ではなくアーキテクチャ的です。
どの言語も、悪い要件、曖昧なドメインモデル、あるいは「完了」の定義に合意できないチームを直せるわけではありません。不変性は混乱したワークフローを理解しやすくはしませんし、「関数型」のコードでも間違ったビジネスルールをよりきれいに表現しているだけということは起こり得ます。
システムが既に本番で動いているなら、これらの考えを全面的な書き換えと扱わないでください。リスクを下げる最小限の動きを探しましょう:
目標は純度ではなく、変更ごとの驚きの数を減らすことです。
これは言語やフレームワーク、チーム構成を変えずに適用できるスプリントサイズのチェックリストです。
簡潔さと簡単さの違い、状態管理、値指向設計、不変性、時間に基づく履歴がデバッグや運用にどう役立つかに関する資料を探してください。
簡潔さは後付けの機能ではなく、小さく繰り返せる選択の中で実践する戦略です。
複雑さは「合理的に見える小さな決定」が積み重なって生じます(追加のフラグ、キャッシュ、例外処理、共有ヘルパーなど)。
良い兆候は「小さな変更」が複数のモジュールやサービスで同時に修正を必要とする場合や、レビューで安全性を判断するために部族的な知識が頼られているときです。
ショートカットは今日の摩擦を最小化します(今すぐ出荷できること)が、そのコストを将来に先送りします:デバッグ時間、調整のオーバーヘッド、変更リスクなどです。
設計やPRレビューで役立つ習慣は「これが導入する新しい動く部品や特別扱いは何か?それを誰が保守するのか?」と問うことです。
デフォルトはエンジニアがプレッシャー下で何を選ぶかを形作ります。ミューテーション(可変)がデフォルトなら共有状態は広がります。メモリ内保持が当然ならトレーサビリティは失われます。
安全な経路が最も抵抗の少ない道になるよう、境界での不変データ、明示的なタイムゾーン/ヌル/リトライ、明確な状態所有権などをデフォルトにしましょう。
「状態」は時間とともに変わるあらゆるものです。問題は、変化が不一致の機会を作ること:二つのコンポーネントが異なる“現在”を持ち得ます。
バグはタイミング依存の挙動(ローカルでは動くのに本番でフラッキーに動く)として現れることが多く、問いは「どのバージョンのデータに基づいて行動したのか?」になります。
不変性とは、値をその場で編集しないこと—更新は既存の値を変えるのではなく、新しい値を作ることです。
実務的には:
常にではありません。ミューテビリティ(可変性)は封じ込められている場合に有用です:
重要なのは境界を越えてミューテーブルな構造を漏らさないことです。
競合状態は通常、共有され変更可能なデータを複数のワーカーが読み書きすることから生じます。
不変性は、ライターが共有オブジェクトを編集する代わりに新しいバージョンを生成することで、調整(coordination)の表面積を減らします。現在のバージョンを公開するルールは依然として必要ですが、データ自体が“動く標的”でなくなるため予測可能性が向上します。
事実(facts)は起こったことの追記式記録(append-only)として扱い、現在の状態(views)はそれらの事実から派生して作る、という分離を指します。
漸進的に適用するには:
情報をプレーンな、明示的なデータ(値)として保存し、そのデータに対して振る舞い(処理)を行う設計です。ストレージに実行可能ルールを埋め込むのは避けます。
これが結合度を下げる理由:
頻繁に変更されるワークフローを一つ選び、次の三つを試してください:
成功の指標は、フラッキーなバグの減少、変更あたりの影響範囲の縮小、リリース時の“注意深い調整”の減少です。