Hackを掌握する極限の知見:Readonly Collectionsがもたらす不変性の極限とアーキテクチャの真実
テックリードの私だ。コードレビューのたびに、ミュータブルなコレクションの意図せぬ書き換えによるバグに頭を悩ませてはいないか?
「この配列、どこで書き換わったんだ?」
「非同期処理の途中でデータがスナップショットから乖離した」
こうした悪夢は、言語の型システムに正面から向き合うことで完全に駆逐できる。今回は、Hack言語の真骨頂である `Readonly Collections` に焦点を当て、HHVMのメモリモデルの裏側から実務で即座に使える堅牢な設計パターンまで、一切の妥協なく叩き込む。
—
1. なぜ「イミュータブル(不変)」ではなく「リードオンリー」なのか?
まず、言語の基本思想を正しく理解してほしい。Hackにおける `ConstVector` や `ConstMap` といった従来の「読み取り専用インターフェース」は、あくまでも「そのインターフェース経由では書き換えができない」というラッパーに過ぎない。元の参照を持っている別の場所から、ミュータブルな操作が行われれば、データは普通に書き換わる。
これに対し、Readonly Collections は型システムとHHVMのランタイムが一体となって保証する。
変数のライフサイクルや参照そのものを「読み取り専用(readonly)」の制約で縛り上げ、コンパイル時に違反を完全に検知する。
HHVMアーキテクチャの視点
HHVMのJITコンパイラは、データが `readonly` としてマークされている場合、エイリアシング(同一メモリ領域を複数の変数が指すこと)に起因する無駄なコピーや、ガード命令の挿入を最適化で排除できる。つまり、安全性が向上するだけでなく、パフォーマンス上のメリットも大きい ということだ。
—
2. 【アンチパターン】なぜそのコードは危険なのか
現場でよく見かける、ミュータブルな依存関係を断ち切れていない設計を見てみよう。
// 【NGな設計】ミュータブルなコレクションの野放し
class UserSessionManager {
private Map
public function getData(): Map
// 呼び出し元が自由に変更できてしまう!
return $this,this->sessionData;
}
}
このコードの罪深さは、カプセル化が完全に破綻している点にある。`getData()` の返り値を叩いた外部のコードが勝手に中身を書き換えれば、セッション管理は崩壊する。ディープコピーを毎回行うのは、CPUサイクルの無駄であり、大規模なトラフィックを扱うシステムでは致命傷になる。
—
3. 実践:Readonly Collectionsによる堅牢なデータフロー設計
ここからが本題だ。Readonly Collectionsを活用し、コンパイル時に副作用を完全に封じ込めたプロダクションコードの模範解答を示す。
以下のコードは、非同期API連携と厳格な型チェックが交差する現代的なHackアプリケーションのコンポーネント設計だ。そのまま脳内トレースしてほしい。
<
namespace App\Architecture;
use namespace HH\Lib\{C, Vec, Str};
/
- ユーザーの権限情報を表すイミュータブルな構造体
/
<<__Rx, __NoDynamic>>
readonly class UserContext {
public function __construct(
public string $userId,
public Vector
) {}
}
/
- 注文データの処理パイプライン
/
<<__Rx>>
final class OrderPipeline {
/
- 外部APIから取得した生データを、Readonlyなコレクションとして安全にラップして返す
- @param vec
$rawPermissions
/
public async function createUserContextAsync(
string $userId,
vec
): Awaitable
// Vectorを readonly として生成・構築
// 型チェッカーにより、このスコープを抜けた後の破壊的変更が完全に阻止される
$perms = Vector::fromItems($rawPermissions);
// ビジネスロジックによる加工(readonlyコンテキストでの操作)
// ここで許可されていないメソッド(add() など)を呼ぶと、型チェッカーが即座にエラーを吐く
return readonly new UserContext($userId, $perms);
}
/
- 権限の検証(副作用ゼロの純粋関数として振る舞う)
/
public function validateAccess(
readonly UserContext $context,
string $requiredPermission,
): bool {
// readonlyコレクションに対する安全な走査
foreach ($context->permissions as $perm) {
if ($perm === $requiredPermission) {
return true;
}
}
return false;
}
}
この設計のキモ
1. `readonly class` と `readonly` 修飾子: インスタンス自体のプロパティ書き換えも型レベルで禁止する。
2. `Vector` の readonly 渡し: `readonly UserContext` の中にある `Vector` は、外部から一切ミュータブルな操作を受け付けない。
3. 副作用の完全な排除: 非同期処理 (`Awaitable`) を経由しても、データの不変性がスレッドセーフ(あるいはリクエストスコープ内での安全性)に保たれる。
—
4. コードレビューで指摘すべきポイント
もし、あなたのチームのメンバーが以下のようなコードを書いたら、即座に差し戻せ。
- `readonly` の伝播漏れ:
関数やメソッドの引数・戻り値に `readonly` がついておらず、結局内部でミュータブルにキャストし直しているケース。これでは型安全性の意味がない。
- 不要なクローン(`->toVector()` などの乱用):
Readonly Collectionsを正しく使っていれば、安全性を担保するための無駄な防衛的コピーは不要になる。パフォーマンス劣化の温床を見逃すな。
—
5. まとめ
Hackの `Readonly Collections` は、単なる「便利なシンタックスシュガー」ではない。
「データは変更されないもの」という強い制約をコードの構造に強制し、バグの温床である「予期せぬ状態変化」を地球上から消し去るための最強の武器である。
大規模なWebアプリケーション、複雑な非同期API連携、そしてシビアなパフォーマンスが要求される現場において、この知見は君のコードベースを揺るぎないものにするだろう。
次のプルリクエストから、さっそく取り入れていけ。妥協のないコードを期待している。