目次を表示する

コードの考古学 ── 設計思想に宿る哲学の源流

第8話 同じ川に二度 ── ヘラクレイトスの流転が不変性を生んだ

場面

紀元前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は、別の値だ。それが正しい。川には二度と同じ水が流れないのだから。


問い:今あなたが「状態を変更している」コードは、古い値を上書きしているか、新しい値を生成しているか。並行アクセスが起きたとき、その二つの振る舞いは根本的に異なる。