Hackの『Readonly Collections』:不変性を保証するデータ構造の設計とHHVM内部メカニズム
HHVM(HipHop Virtual Machine)のアーキテクチャおよびHackの厳格な静的型システム(Strict Mode)の設計に長年携わってきた者として、現代の大規模分散システムにおける最大の敵が「意図しない状態の共有と副作用」であることを、我々は痛いほど知っている。
PHPの歴史的遺産を引き継ぎつつ、大規模なコードベースの堅牢性と極限のパフォーマンスを両立させるためにHackが到達した回答の一つが、Readonly Collections である。
本稿では、単なる文法解説ではなく、コンパイラの型チェッカー、HHVMのオブジェクトモデル、そしてJITコンパイルにおけるメモリ最適化の観点から、Readonly Collectionsの真の姿を暴いていく。
—
1. 従来のイミュータビリティの幻想とコスト
多くの言語において、「不変(Immutable)」という概念はコストを伴う。例えば、オブジェクトや配列をコピー(`clone` やスプレッド構文)して変更不可能な状態を作るアプローチは、O(N)のメモリコピーコストとGC(ガベージコレクション)への負荷増大を招く。
Hackが提供する `ImmVector` や `ImmMap` などのイミュータブルコレクションは、確かに「コレクション自体の構造変更」を防ぐことはできた。しかし、それらは「要素内部のミュータビリティ」までを型レベルで縛ることはできなかったのだ。
// 従来のImmVectorの限界
<<__Strict>>
images = ImmVector { new ImageProcessor() };
// ImmVector自体は再代入や要素の追加ができないが…
// 要素が保持する内部状態の変更は防げない
images[0]->setQuality(80); // これは静的型チェッカーをすり抜けてしまう
この「参照透過性の欠如」をコンパイルタイムで完全に根絶するために設計されたのが、Readonly Collections(読み取り専用コレクション) および `readonly` 修飾子である。
—
2. 型チェッカーとHHVMランタイムにおける Readonly の正体
Hackの型チェッカー(`hh_client`)にとって、`readonly` は単なるコーディング規約ではない。これは、「エイリアシング(Aliasing)のコントロール」 という極めて高度な型理論の応用である。
型システムにおける Readonly 伝播
Hackの厳格モード(`<<__Strict>>`)において、`readonly` キーワードは値ではなく 「アクセスパス(Access Path)」 に付与される。
<<__Strict>>
class DataNode {
public function __construct(public string $payload) {}
}
async function process_data(readonly Vector
// コンパイルエラー: readonlyなコレクションの要素は readonly として扱われるため、
// 非readonlyなメソッドの呼び出しやプロパティの書き換えが静的に拒否される。
// $nodes[0]->payload = “mutated”;
// 正しいアクセス
echo $nodes[0]->payload;
}
HHVMの内部(C++層)において、オブジェクトやコレクションはポインタとして管理されている。もし、あるミュータブルなデータ構造に対して、複数のコンポーネントが同時に書き込み権限を持つと、JITコンパイラ(RepoAuthoritativeモード含む)は最適化の際にアリーシング(エイリアス解析の失敗)を考慮せざるを得なくなり、レジスタ割当てやインライン展開の効率が著しく低下する。
しかし、`readonly` 修飾子が付与されたデータ構造は、「このスコープおよび派生するアクセスパスにおいて、書き込みは一切行われない」 ことが保証される。
—
3. メモリ最適化とJITの視座:なぜ Readonly は高速なのか?
HHVMのTC(Translation Cache)とJITコンパイラは、型情報と不変性に基づいてネイティブコードを生成する。
1. ライト・バリア(Write Barrier)の排除:
HHVMは世代別GCを採用している。ミュータブルなオブジェクトのプロパティ変更やコレクションへの要素追加には、世代間参照を追跡するためのライト・バリアが挿入される。しかし、`readonly` コンテキストが保証された領域では、ポインタの書き換えが発生しないため、JITはこのライト・バリアの機械語命令を完全に排除できる。
2. ゼロコピーの安全性:
関数間でデータを渡す際、従来の言語では防御的コピー(Defensive Copy)を行うか、あるいは予期せぬ改変のリスクを許容していた。Hackの Readonly Collections は、ポインタをそのまま(Zero-copyで)安全に渡すことを可能にする。なぜなら、型チェッカーが「呼び出し先での変異」をコンパイル時に完全に封じ込めているからだ。
—
4. 実践:Readonly Collections を用いた堅牢なデータフロー設計
では、実際のシステムアーキテクチャにおいて、どのように Readonly Collections を設計し、恩恵を受けるべきか。具体的なコードで示そう。
<<__Strict>>
namespace Architecture\Core;
// ドメインモデルの定義
final class TransactionRecord {
public function __construct(
public readonly int $id,
public readonly float $amount,
public readonly string $currency,
) {}
}
class LedgerProcessor {
// 入力として readonly な Vector を要求する
// これにより、呼び出し元が保持しているコレクションの整合性が保証される
public function calculateTotal(readonly Vector
Shapes::idx(shape(), ”); // dummy
$total = 0.0;
foreach ($records as $record) {
// $record 自体も readonly コンテキストに伝播する
$total += $record->amount;
}
return $total;
}
}
<<__EntryPoint>>
function main(): void {
// ミュータブルな構築用ベクター
$builder = Vector {
new TransactionRecord(1, 150.00, ‘USD’),
new TransactionRecord(2, 200.50, ‘USD’),
};
$processor = new LedgerProcessor();
// 構築完了後、readonly としてキャストまたはそのまま渡す
// ここで型チェッカーは $builder から生成されるアクセスを readonly に昇格させる
$total = $processor->calculateTotal(/ readonly / $builder);
echo “Total Amount: {$total}\n”;
}
この設計がもたらす優位性
- スレッドセーフティの精神的・構造的解放: PHP/Hackはリクエストライフサイクル型のシングルスレッド実行モデルだが、非同期処理(Async / Awaitable)が多用される現代のHHVM環境では、並行実行されるコルーチン間で同一オブジェクトが共有されるリスクがある。Readonly Collectionsは、非同期タスク間でのデータの競合や競合状態(Race Condition)を型レベルで無効化する。
- リファクタリング耐性: 「このメソッドが渡されたコレクションを破壊していないか」を人間がコードレビューで追う必要は一切ない。すべては `hh_client` の静的解析結果に委ねられる。
—
5. チーフアーキテクトからの提言
シニアエンジニアやセキュリティ研究者であれば理解できるはずだ。システムの脆弱性の多くは、「意図しない変異(Unintended Mutation)」から生まれる。セキュリティ境界を越えてデータを伝播させる際、ディープコピーのパフォーマンスペナルティを恐れてミュータブルな参照をそのまま渡し、どこかのレイヤーで不正に書き換えられるというバグは、大規模システムにおいて最もデバッグが困難な類のものだ。
Hackの Readonly Collections は、パフォーマンスを犠牲にすることなく、厳格な数学的保証のもとでデータフローを制御するための究極のツールである。
今すぐコードベースのミュータブルな共有データを洗い出し、`readonly` の要塞で固めよ。コンパイラが味方であるうちに、型システムの限界を限界まで押し上げるのだ。