概要
関数型プログラミングにおいて、「状態を持たない」設計は美徳である。
だがソフトウェアは常に「状態の変化」を伴う──では、どう整合性を保ちつつ状態を表現するか?
その答えが、イミュータブルなデータ構造と、**永続データ構造(Persistent Data Structures)**の存在である。
本稿では Clojure を例に、List・Tree・Map といった構造を「不変であることを前提」にどう設計・操作するかを明快に解説する。
1. データ構造における「不変」の意味
✅ イミュータブルとは
一度作られた値が、外部から変更されないこと。
(def v1 [1 2 3])
(def v2 (conj v1 4)) ; v1は変更されず、新しいベクターv2が返る
-
v1
はそのまま保持 -
v2
はv1
をベースに新しい構造を持つ
2. Clojureの永続データ構造とは?
Clojureでは、すべてのコアデータ構造(list
、vector
、map
、set
)は不変であり、
内部的に「構造共有(structural sharing)」を用いて効率よく更新を実現している。
- O(1) に近い高速性
- 前のバージョンとの参照関係を保ったまま更新可能
3. List の不変操作
(def items '(1 2 3))
(def updated (cons 0 items)) ; => (0 1 2 3)
- 元のリストは変化しない
-
cons
で新しい先頭付きリストを作成
4. Map の不変操作
(def user {:name "Alice" :age 30})
(def updated-user (assoc user :age 31)) ; => {:name "Alice", :age 31}
-
assoc
は新しいMapを返す -
user
はそのまま保持される
5. Treeのようなネスト構造も同様
(def tree {:val 1 :left {:val 2} :right {:val 3}})
(def new-tree (assoc-in tree [:left :val] 10))
-
assoc-in
はパス指定による構造更新を提供 - 深い構造でも破壊的変更なしで更新可能
6. イミュータブルな構造の利点
観点 | 説明 |
---|---|
履歴の保存 | 古いバージョンがそのまま残る → Undo/Redo が容易 |
スレッド安全 | 状態の共有・変更が不要 → ロックなしで並行処理が可能 |
デバッグ容易性 | 任意時点の状態をそのまま保持可能 |
予測可能性 | 「何がいつどこで変わるか」が存在しないため、ロジックが明示的 |
7. 設計判断フロー
① この構造は状態変更を伴うか? → YES → イミュータブル構造で包む
② 更新履歴を保持したいか? → YES → データ構造のバージョンを保持する戦略に
③ パフォーマンスが気になる? → YES → 永続構造による構造共有を導入
④ ネストが深くて複雑? → YES → assoc-in / update-in などを活用
よくある誤解と対策
❌ 不変構造はメモリを大量に消費するのでは?
→ ✅ No。内部的に変更のあった部分だけをコピーし、それ以外は共有される(構造共有)
❌ 可変の方がコードがシンプルでは?
→ ✅ 初期はそう見える。しかしコードが複雑になるほど不変性の恩恵が劇的に効いてくる
❌ リアルタイム系では使えない?
→ ✅ Clojureのデータ構造は極めて効率的。実用性の面でも問題はほぼない
結語
不変なデータ構造とは、設計を「守る」ための戦略である。
- 状態を破壊せず、履歴を持ち
- 並列化・拡張・テストを容易にし
- ソフトウェアを「安定」と「予測可能性」で支える
関数型におけるデータ構造とは、
“状態を制御可能な構造体として、秩序と再現性を担保するアーキテクチャである。”