Hackの`readonly`属性による不変性の強制:副作用を抑えた関数型プログラミングの極意
テックリードの私が生々しいコードレビューの現場で最も多く目にするバグ、それは「意図しないデータの書き換え(副作用)」に起因する競合や状態汚染だ。非同期処理や大規模な並行実行環境において、共有mutable(可変)なオブジェクトほど恐ろしいものはない。
HHVMの深部を知る者として断言しよう。PHPの系譜を引き継ぎながらも、Hackが「厳格な静的型システム(Strict Mode)」とコンパイル時最適化によって到達したのは、ランタイムのオーバーヘッドを極限まで削ぎ落とした安全性の担保だ。
今回は、Hackの `readonly` 修飾子を駆使し、型システムレベルで不変性(Immutability)を強制するプロダクション設計の極意を伝授する。
—
なぜ `readonly` が必要なのか? — 副作用という名の魔物
従来のオブジェクト指向プログラミングでは、ゲッターを用意しつつも、内部のオブジェクト参照が保持されていれば、意図せず値を書き換えられるリスク(Aliasingの問題)がつきまとっていた。ディープコピーを毎回行うのはパフォーマンスの自殺行為であり、HHVMのJITコンパイラであっても無駄なアロケーションはキャッシュ効率を悪化させる。
Hackの `readonly` は、単なる「再代入不可(`const`)」ではない。
「その参照を通じて指し示されるデータ構造全体の書き換えを、型チェッカーが静的に禁止する」 という強力な契約である。
型チェッカーとHHVMの裏側
Hackの型チェッカー(hh_client)は、コンパイル時に `readonly` コンテキスト伝播を厳密に追跡する。これにより、実行時(Runtime)のオーバーヘッドを完全にゼロに抑えながら、メモリ上のデータ構造がイミュータブルであることを保証できる。HHVMは、これがreadonlyであると分かっていれば、安全にメモリ領域を共有(Copy-on-Writeの最適化など)でき、マルチスレッドや非同期タスク間のデータ受け渡しにおけるボトルネックを粉砕できる。
—
プロダクションコード例:堅牢な非同期APIパイプライン
実際のWebアプリケーションで、外部APIからのレスポンスデータを加工し、非同期で複数のコンポーネントへディスパッチするシナリオを考えてみよう。
以下のコードは、`readonly` を活用してデータの不変性をコンパイル時に担保した、保守性の高いプロダクションコードの模範解答だ。
hh
<
namespace Hack\Expert\Immutability;
/
- ユーザープロフィールの不変データ構造
- プロパティ自体も readonly にし、全体としてのイミュータブルを強制する。
/
readonly class UserProfile {
public function __construct(
public int $id,
public string $username,
public Map
) {}
}
/
- 注文データの不変構造体
/
readonly class OrderContext {
public function __construct(
public string $orderId,
public UserProfile $user,
public float $amount,
) {}
}
final class OrderProcessor {
/
- 外部から渡された読み取り専用コンテキストを受け取り、
- 副作用なしで新しい状態を構築する。
- @param readonly OrderContext $context
- @return readonly OrderContext 完全に隔離された新しいコンテキスト
/
public async function processAsync(
readonly OrderContext $context,
): Awaitable
// ログ出力などの副作用は明確に境界の内側に閉じ込める
\CSI\Logger::info(‘Processing order: ‘ . $context->orderId);
// 非同期処理の模擬 (例: 外部決済APIコール)
await \HH\Asio\usleep(100000);
// 【アンチパターン】
// $context->amount = 100.0;
// -> 型エラー: Cannot modify a readonly property (Hack#4128)
// 不変性を維持したまま、データを変形した「新しいインスタンス」を返す
// Hackの構造的サブタイピングとreadonlyコンテキストの伝播を利用
return new OrderContext(
$context->orderId,
$context->user,
$context->amount 1.10, // 税込み計算の適用
);
}
}
このコードの優れた設計ポイント
1. `readonly class` と `readonly` 引数
関数シグネチャに `readonly` を付与することで、呼び出し元に対して「この関数はこのデータを絶対に破壊しない」という強い保証を型レベルで行っている。
2. 副作用の完全な隔離
データ構造の書き換えを一切禁止することで、複数の非同期タスクが同一の `OrderContext` を安全に参照(Share)できる。ロック機構(Mutex)のオーバーヘッドは不要だ。
3. 安全な変形(Transformation)
データを変更する代わりに、新しいインスタンスを生成して返す(Functional Update)。HHVMのメモリマネージャは短命なオブジェクトの回収に非常に最適化されているため、このアプローチはパフォーマンス面でも極めて有利である。
—
コードレビューの現場から:よくある非効率な実装と改善策
❌ 駄目な例:参照の漏洩とミュータブルな混在
// 【アンチパターン】
class BadOrderManager {
private OrderContext $context;
public function __swallow(OrderContext $ctx): void {
// 内部プロパティにそのまま代入。
// 呼び出し元で $ctx が書き換えられると、こちらの状態も壊れる(Aliasingの罠)
$this->context = $ctx;
}
}
なぜ非効率なのか?
ミュータブルな参照をそのまま保持すると、どこで値が書き換わったのか追跡が不可能になる(いわゆる「Spaghetti State」)。これのデバッグに何時間も費やすのは、エンジニアのキャリアの無駄遣いだ。
⭕ 正しい例:`readonly` による境界線の死守
コンポーネント間でデータを渡すときは、必ず `readonly` キーワードを境界線(Boundary)としてインターフェースに組み込め。
interface IReadonlyRepository {
public function find(int $id): Awaitable;
}
これにより、リポジトリ層から返されたデータが、プレゼンテーション層やビジネスロジック層で誤って書き換えられる事故を、実行時ではなく静的解析の段階(hh_client)で100%ブロックできる。
—
結びにかえて:型を味方につける者だけが、スケールするシステムを作れる
Hackの `readonly` 属性は、単なるシンタックスシュガーではない。それは、複雑化するWebアプリケーションの並行処理や非同期I/Oの嵐の中において、コードベースの秩序を保つための「絶対的な防壁」である。
「動けばいい」という妥協を捨て、型チェッカーを最高の相棒として使い倒せ。そこに広がるのは、予測可能性に満ちた、美しくスケーラブルなエンジニアリングの世界だ。さあ、今すぐコードベースの `readonly` 率を高めに行こう。