不変性

データを変更せず、新しいデータを作成するプログラミングの原則

プログラミング関数型プログラミング

不変性とは

不変性 (Immutability) は、一度作成したデータを変更せず、変更が必要な場合は新しいデータを作成するプログラミングの原則である。関数型プログラミングの核となる概念で、バグの削減と並行処理の安全性に寄与する。

ミュータブル vs イミュータブル

ミュータブルと イミュータブル のコード例を比較する。

// ❌ ミュータブル: 元のオブジェクトを変更
const user = { name: 'Alice', age: 30 };
user.age = 31; // 元のオブジェクトが変わる

// ✅ イミュータブル: 新しいオブジェクトを作成
const updatedUser = { ...user, age: 31 };
// user は変わらない、updatedUser は新しいオブジェクト

配列の不変操作

配列の不変操作のコード例を示す。

const items = [1, 2, 3];

// ❌ ミュータブル
items.push(4);        // 元の配列を変更
items.sort();         // 元の配列を変更

// ✅ イミュータブル
const added = [...items, 4];           // 新しい配列
const sorted = [...items].sort();      // コピーしてソート
const filtered = items.filter(x => x > 1); // 新しい配列
const mapped = items.map(x => x * 2);     // 新しい配列

[...items].sort() はコピーしてから並べ替える定石だが、コピーを返す配列メソッド (toSorted toReversed toSpliced with) が 2023 年に標準化されたため、同じことを 1 呼び出しで書ける。MDN の互換表では 2023 年 7 月以降の主要ブラウザで利用できる。とくに items.with(0, 9) は 0 番目だけを差し替えた新しい配列を返すので、map の中でインデックスを比較する定型コードが不要になる。

なぜ不変性が重要か

データが変わらないため挙動が予測可能になり、デバッグ時に変更履歴を追跡しやすい。共有データの競合が起きないため並行処理にも安全だ。React では参照比較で変更を検出するため、不変性を守ることで再レンダリングの最適化が効く。

React と不変性

React と不変性のコード例を示す。

// React は参照比較で再レンダリングを判断
const [items, setItems] = useState([1, 2, 3]);

// ❌ ミュータブル: React が変更を検出できない
items.push(4);
setItems(items); // 同じ参照 → 再レンダリングされない

// ✅ イミュータブル: 新しい参照 → 再レンダリング
setItems([...items, 4]);

判定の実体は参照の同一性である。React の公式ドキュメントは、渡した新しい値が現在の状態と Object.is で同一なら再レンダリングを省くと明記している。配列を直接 push して同じ参照を渡すと、中身が変わっていても「変化なし」と見なされる。React.memouseMemo の依存配列も同じ比較なので、参照を変えない更新は下位コンポーネントにも伝わらない。

Rust の不変性

Rust の不変性のコード例を示す。

// Rust はデフォルトで不変
let x = 5;
// x = 6; // コンパイルエラー

let mut y = 5; // mut で明示的に可変にする
y = 6; // OK

不変なのは束縛であって、値そのものが凍結されるわけではない。mut を付けない変数への再代入はコンパイルエラー (E0384) になるが、CellRefCellMutex を経由する内部可変性を使えば、不変参照のまま中身を書き換えられる。Rust の主眼は「変更を禁じること」ではなく「可変にする場所を型として宣言させること」にある。

DynamoDB と不変性

DynamoDB と不変性のコード例を示す。

// イベントソーシング: データを変更せず、イベントを追記
await db.put({
  TableName: 'events',
  Item: {
    entityId: 'order-123',
    eventType: 'OrderCreated',
    timestamp: Date.now(),
    data: { amount: 1000 },
  },
});
// 過去のイベントは変更しない → 監査証跡が完全

ただし追記のみを守るのは書き手の作法だけでは足りない。同じキーの上書きを ConditionExpression: 'attribute_not_exists(pk)' で弾き、書き込み用の IAM ロールから UpdateItemDeleteItem を外しておけば、あとから履歴を書き換える経路そのものが無くなる。監査証跡が完全だと言えるのは、そこまで機構で塞いだ場合である。

落とし穴: コピーは浅い

{ ...user }[...items] が複製するのは最上位の階層だけである。入れ子のオブジェクトは元と同じものを指したままなので、コピーしたつもりで書き換えると元のデータも変わる。

const user = { name: 'Alice', profile: { age: 30 } };

// ❌ 浅いコピー: profile は共有されたまま
const copy = { ...user, name: 'Bob' };
copy.profile.age = 31;  // user.profile.age も 31 になる

// ✅ 変更する経路のオブジェクトだけ作り直す
const next = { ...user, profile: { ...user.profile, age: 31 } };

Object.freeze も浅い。凍結されるのは直接のプロパティだけで、入れ子のオブジェクトは変更できてしまう。値の全体を切り離したいときは structuredClone で深いコピーを作れるが、関数のように直列化できない値が含まれていると DataCloneError になり、大きな構造ではコピーのコストも無視できない。階層が深くなってきたら、内部で構造共有を行うライブラリ (Immer など) に任せるほうが、手書きの入れ子スプレッドより事故が少ない。

不変インフラストラクチャ

サーバーを更新するのではなく、新しいサーバーを作成して置き換える。詳細は「不変インフラストラクチャ」を参照。

現場での応用を知るには関連書籍も役立つ。

この記事は役に立ちましたか?

関連用語

関連する記事