イミュータビリティ
Immutability
一度作った値を書き換えず、変更を新しい値の生成で表す方針。共有と並行処理に強いコードの土台になる。
概要
イミュータビリティ(不変性)とは、一度作った値を後から書き換えず、「変更したければ新しい値を作る」ことで変化を表す方針です。たとえば多くの言語で文字列は不変で、upper() のようなメソッドは元の文字列を書き換えるのではなく、大文字化された新しい文字列を返します。この「元は残したまま、変わった版を新しく作る」という発想を、文字列だけでなくリストやオブジェクトなどあらゆるデータに広げたものがイミュータビリティです。
この方針は関数型プログラミングの中心的な考え方として発展しましたが、いまでは特定のパラダイムに限らない設計の常識になりつつあります。React の状態更新、Redux などの状態管理、Git のコミット履歴、イベントの追記だけで状態を表すイベントソーシングまで、「書き換えない」ことを土台にした仕組みは至るところにあります。
直感的には「書き換えたほうが速くて簡単なのに、なぜわざわざ作り直すのか」と感じるかもしれません。その答えは、書き換え(ミューテーション)が引き起こす事故の種類を知ると見えてきます。
なぜ生まれたか
多くの言語では、オブジェクトを変数に代入したり関数に渡したりするとき、実体はコピーされず参照だけが渡ります(値渡しと参照渡し)。つまり1つのオブジェクトを複数の場所が同時に「持って」いるのが普通の状態です。ここで誰か1人がそのオブジェクトを書き換えると、参照を持つ全員の見ている値が一斉に変わります。書き換えた本人には自分のコードしか見えていないのに、影響はプログラムの遠く離れた場所に波及する — 「渡したはずの設定オブジェクトがいつの間にか壊れている」「関数を呼んだだけで引数が変わっていた」という、原因箇所と症状箇所が離れた追いにくいバグの典型です。
並行処理になると問題はさらに深刻になります。複数のスレッドが同じ可変データを同時に読み書きすると、タイミング次第で結果が変わる競合状態(レースコンディション)が生じ、ロックによる排他制御が必要になります。ロックは正しく書くのが難しく、デッドロックや性能低下の温床です。突き詰めると、これらの問題の根はすべて「共有された可変状態」にあります。共有をやめるか、可変をやめるかのどちらかで問題は消える — イミュータビリティは後者、「そもそも書き換え不可能にすれば、書き換え事故も競合も原理的に起こらない」という解決です。
詳細
変更とは新しい値を作ること
イミュータブルな世界では、「リストに要素を追加する」は「要素が1つ多い新しいリストを作る」ことを意味します。元のリストはそのまま残るため、参照を持っていた人たちの見ている値は絶対に変わりません。変化はすべて「古い値 → 新しい値」という置き換えとして表れるので、「いつ・どこで値が変わったか」が代入箇所として明示的になり、変更履歴(アンドゥ、タイムトラベルデバッグ)も過去の値を捨てずに持っておくだけで実現できます。
構造共有 — 毎回まるごとコピーするわけではない
「変更のたびに作り直すなんて、巨大なデータでは遅すぎるのでは」というのが当然の疑問です。ここで効くのが構造共有(structural sharing)という技法です。不変なデータは絶対に変わらないと保証されているからこそ、新しいバージョンは変わっていない部分を旧バージョンと安全に共有できます。たとえば木構造で1つの葉を変更するなら、根からその葉までの経路上のノードだけを作り直し、残りの枝はすべて旧版のものをそのまま指せばよいのです。この考え方で実装されたデータ構造は永続データ構造(persistent data structure)と呼ばれ、Clojure や Scala の標準コレクション、JavaScript の Immutable.js などで、要素数 n に対しておおむね log n 程度のコストで「変更された新しい版」を作れます。
変更検知が参照比較で済む
イミュータビリティのもう1つの大きな恩恵は、「変わったかどうか」を一瞬で判定できることです。可変なオブジェクトでは、変わったかを知るには中身を隅々まで比較する(深い比較)しかありません。しかし「変更=新しいオブジェクトの生成」というルールが守られていれば、参照が同じ=中身も同じと言い切れるため、=== の参照比較1回で判定が終わります。React が仮想DOMの再描画をスキップする判断(メモ化)、Redux などの状態管理ライブラリが状態の変化を購読者に通知する仕組み、シグナル系のリアクティビティの依存追跡は、いずれもこの「参照が変わったら中身が変わった」という規約に支えられています。逆に、状態オブジェクトを直接書き換えてしまうと参照が変わらないため変更が検知されない — React 入門者が最初に踏む「setState したのに再描画されない」バグは、まさにこの規約違反です。
言語への広がり
イミュータビリティは Haskell や Erlang のような関数型プログラミング言語では標準(むしろ唯一の選択肢)でしたが、その利点が広く認められ、主流の言語にも輸入されてきました。JavaScript の const(再代入の禁止。中身の不変までは保証しない点に注意)と Object.freeze、Java の record や String の不変性、C# の record と init 専用プロパティ、Python の tuple や frozen な dataclass、Rust に至っては「変数はデフォルトで不変、書き換えたければ mut を明示」という逆転の設計です。API 設計の面でも、「引数を書き換えず新しい値を返す関数」(JavaScript の toSorted など)が破壊的メソッドの代替として追加される流れが続いています。
トレードオフ — コピーコストと使いどころ
万能ではありません。構造共有があっても、新しいオブジェクトの生成は書き換えよりメモリ確保が多く、短命なオブジェクトを大量に作るためガベージコレクションへの負荷も増えます。数値計算のホットループや巨大な配列の頻繁な更新など、性能が最優先の箇所では可変なデータを局所的に使うのが現実解です。実践的な指針は「共有される境界では不変、関数の内部に閉じた一時変数は可変でもよい」という使い分けです。外に見える状態を不変に保てば事故の波及は防げますし、Clojure の transient や Immer のように「内部では効率よく書き換え、完成品を不変として公開する」折衷の仕組みも整っています。不変をデフォルトにし、可変を例外として意識的に選ぶ — それが現代的なバランスです。
