【実務・中級編】Hackの『Readonly』属性による不変性の強制:副作用を抑えた関数型プログラミングの導入 – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

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 $metadata,
) {}
}

/

  • 注文データの不変構造体

/
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` 率を高めに行こう。

タイトルとURLをコピーしました