場面
紀元前500年頃のエフェソス。エーゲ海を望むこの港町で、ヘラクレイトスは川を眺めていた。
後世に「断片91」と呼ばれる言葉が彼の中で結晶しつつあった。「同じ川に二度入ることはできない。なぜなら常に新しい水が流れ込んでくるから」。しかし彼が言いたかったのは単純な事実——川の水は流れる——ではなかった。もっと根本的なことを彼は見ていた。
「万物は流転する(パンタ・レイ)」。安定しているように見えるものも、変化しているものも、その本質は変化そのものだ。火こそがヘラクレイトスの宇宙のメタファーだった。燃え続けることで形を保つ炎。静止した状態では存在できない。存在とは常に過程(プロセス)だ。
ヘラクレイトスが生きた時代から2500年後。あるプログラマーが、まったく異なる文脈で同じ問題に直面していた。
彼が書いていたのはマルチスレッドのサーバーアプリケーションだった。データベースからuserオブジェクトを取得し、メールアドレスを更新しようとする。コードは単純に見えた。
user = db.get_user(user_id)
user.email = new_email # 状態を変更する
db.save(user)
ところがある夜、本番環境で奇妙なバグが発生した。二人のユーザーが同時に自分のプロフィールを更新すると、片方の変更が消えた。デバッグに3日かかった。原因:二つのスレッドが同じuserオブジェクトへの参照を持っており、一方がemailを書き換えている間に、もう一方が古い状態を保存した。
「同じ川に二度入れない」のに、両者は「同じオブジェクト(川)」に入ろうとしていた。川は変わっているのに、そのことを知らずに。
答え
状態は「更新される」のではなく、「時間とともに変化する値の系列」として扱うべきだ。変化を「上書き」としてモデル化する試みは、時間の流れを無視しており、並行アクセスで必ず破綻する。「ヘラクレイトスの川」を「同じ川」だと思い込む誤謬——それが可変状態(mutable state)の根本的な問題だ。
第1話で扱ったテセウスの船は、まさにヘラクレイトスの川を造船所で再現した思考実験だった。板が一枚ずつ流れていく——同じ船は二度ない、はずだ。それでも私たちが「テセウスの船」と呼び続けられるのは、ID(同一性のスレッド)が変化を貫いて続いているからだった。同じ構造が状態管理にも当てはまる:川は流れ続けるが、流れを記録した「事実の列」は不変として保存できる。それが第3話のイベントソーシングであり、本章で扱うイミュータブル設計の原理だ。
CSへの翻訳
1990年代後半から2000年代にかけて、関数型プログラミングはこの問題の答えとして再注目された。HaskellやErlangは、言語の設計として状態の変更を禁止した。値(Value)は生成後に変更できない。何かを「変えたい」ならば、変えた後の新しい値を作る。古い値はそのまま残る。
Rich Hickeyは2007年にClojureを設計するにあたって、この思想を「状態は時間軸上の値の継承だ(State is a series of values over time)」と言語化した。Clojureのatomは可変変数ではない——それは不変な値への参照であり、新しい値に「差し替える」操作のみが許される。古い値を読んでいたコードは、差し替え前の古い値を引き続き安全に使える。川は流れているが、過去のある時点の川の「スナップショット」は失われない。
2015年、Dan Abramovが発表したReduxはReactコミュニティにこの思想を持ち込んだ。UIの状態は不変なオブジェクトとして保持される。変更(Action)が発生すると、Reducer(純粋関数)が古い状態と変更内容を受け取り、新しい状態を作って返す。古い状態は壊されない。Redux DevToolsで「タイムトラベルデバッグ」ができるのはこのためだ——過去のどの時点の状態にも戻れる。なぜなら過去の状態が保持されているから。
イベントソーシング(第3話で触れたテーマ)もヘラクレイトス的だ。現在の口座残高を「残高フィールドを上書き更新する」ではなく、「入金・出金イベントの列を積み重ね、残高は派生する」と考える。川そのものは流れ続けるが、どの時点でも水の量(残高)を計算できる。
Reactのrender関数も本質的に不変性の思想の上にある。コンポーネントのUIは「現在の状態の純粋関数」として記述される。DOMを直接「変更」するのではなく、新しい状態からUIを「導出」する。ブラウザのDOMはReactが差分を適用して実際に更新するが、それはプログラマーの関心事ではない。プログラマーは「状態XのときUIはYだ」という不変の関係を書く。
設計への示唆
並行処理でのバグを「難しい問題」として諦めるのではなく、「可変状態を共有しようとしたから起きた問題」として位置づけ直す——これがヘラクレイトスの教えのソフトウェア的な含意だ。
ミュータブルな変更(mutation)は「同じ川に二度入れる」という仮定の上に立っている。Aさんが見ているuserオブジェクトと、Bさんが見ているuserオブジェクトが「同じもの」だという仮定だ。しかしその仮定は時間が流れた瞬間に崩れる。
❌ 可変状態の共有(同じ川に二度入ろうとする)
// スレッドAとスレッドBが同じuserオブジェクトを参照している
const user = getUser(userId); // { email: "[email protected]" }
// スレッドAが変更する
user.email = "[email protected]";
// スレッドBは古い状態を読んで保存してしまうかもしれない
db.save(user); // どのuserを保存している?
✅ 不変な値の変換(新しい川を作る)
// 元の値は変更しない
const originalUser = getUser(userId); // { email: "[email protected]" }
// 新しい値を作る
const updatedUser = { ...originalUser, email: "[email protected]" };
// originalUserは変わっていない
// updatedUserは新しい事実
db.save(updatedUser);
originalUserを参照していたコードは、更新後も安全にoriginalUserを使える。川は流れているが、あなたがすでに手に取った水(値)は変わらない。
時間を明示することが、ヘラクレイトス的設計の鍵だ。「ユーザーのメールアドレスを変更する」ではなく、「時刻TにおけるユーザーのメールアドレスはXだった、時刻T+1にはYになった」という事実を積み重ねる。両方の事実は同時に真であり続ける。
sequenceDiagram
participant A as スレッドA
participant B as スレッドB
participant S as 状態ストア
Note over A,S: ❌ 可変状態(mutation)
A->>S: user = getUser() → {email: "old"}
B->>S: user = getUser() → {email: "old"} ← 同じ参照
A->>S: user.email = "new" ← 上書き!
B->>S: save(user) ← どのuserを保存?競合発生
Note over A,S: ✅ 不変な値の連鎖(immutable values)
A->>S: v1 = getUser() → {email: "old", version: 1}
B->>S: v1 = getUser() → {email: "old", version: 1} ← 同じ不変値
A->>S: v2 = {...v1, email: "new"} → 新しい値を生成
A->>S: save(v2) ← v1は変更されていない
B->>S: save(v1 with check) ← v1は安全に使える
イミュータビリティは「制約」ではなく「正直なモデル」だ。時間は実際に流れている。状態は実際に変わる。それを「上書き更新」として表現するのは、川が「同じ川のまま変わった」という矛盾した記述だ。「古い値をそのままに、新しい値を作る」は、時間の流れを正直に表現している。
ヘラクレイトスは「変化こそが唯一の定数だ」と言った。その意味でイミュータブルなプログラミングは、変化を否定しない——変化を「値の交代」として透明に表現する。あなたが昨日参照していたuserと、今日参照しているuserは、別の値だ。それが正しい。川には二度と同じ水が流れないのだから。
問い:今あなたが「状態を変更している」コードは、古い値を上書きしているか、新しい値を生成しているか。並行アクセスが起きたとき、その二つの振る舞いは根本的に異なる。