コードレビューは戦場だ:Union Typesを甘く見るな
チームのプルリクエストを見ていると、時折こういうコードに出くわす。
// [Bad Example] 典型的な“思考停止”型分岐
function handle_payload(ContrivedPayload $payload): void {
$data = $payload->getData();
if ($data is string) {
// 文字列の処理
} else if ($data is int) {
// 整数値の処理
} else {
// 配列の処理…のはずが、将来型が追加されたら素通りしてバグる
}
}
おいおい、待て。ここはPHPの泥沼ではない。我々はHackを使っているのだ。HHVMの型チェッカー(typechecker)という強力な剣を握りながら、なぜ実行時まで型の網羅性を担保できないような書き方をしているのか。
Strict ModeにおけるUnion Typesとパターンマッチングの制御は、単なるシンタックスの好みではない。「未来の変更に対する耐性」と「HHVMのJIT最適化を極限まで引き出すための契約」なのだ。
今回は、HackのStrict ModeにおけるUnion Typesの網羅的チェックと、それを実務の非同期API連携や複雑なドメインモデルにどう適用すべきか、その極限の知見を授けよう。
—
1. Hackの型チェッカーとUnion Typesの厳格な関係
Hack言語において、`<<__Strict>>` モードでの開発は宗教に近い規律を要求する。あいまいな型(dynamic)の排除はもちろんのこと、Union Types(直和型)を扱う際には、型チェッカーが「すべてのケースが網羅されているか」を静的に検証する。
ここで重要になるのが、HHVMのアーキテクチャ特性だ。HHVMは、静的型情報をもとにJIT(Just-In-Time)コンパイラがネイティブ機械語への最適化を行う。型分岐(`is` 演算子や `match` 式)が網羅的であり、かつ予測可能であるほど、プロファイル誘導最適化(PGO)の精度が跳ね上がり、ディスパッチのオーバーヘッドが消滅する。
逆に、網羅性チェックをサボった動的なフォールバック(`else` 句など)を書くことは、型チェッカーの目を欺くだけでなく、HHVMの最適化パスに対しても悪影響を与える。
—
2. 実践:`match` 式による完全網羅とコンパイル時保証
非同期API連携のレスポンスや、外部サービスからのWebHookペイロードを想像してほしい。データは常に流動的であり、状態は多様化する。
ここでは、APIレスポンスのUnion Typesに対して、`match` 式を用いて「新しい状態が追加された瞬間にコンパイルエラー(型エラー)で気付ける」プロダクションコードの設計パターンを示す。
堅牢なプロダクションコード例
<<__Strict>>
namespace App\API;
// 3つの異なる状態を持つAPIレスポンスのUnion Types(型エイリアス)
type SuccessResponse = shape(‘status’ => string, ‘data’ => array
type ErrorResponse = shape(‘code’ => int, ‘message’ => string);
type RateLimitResponse = shape(‘retry_after’ => int);
type ApiResponse = SuccessResponse | ErrorResponse | RateLimitResponse;
final class ApiProcessor {
/
- Union Typesをmatch式で完全に網羅し、処理する。
- もしApiResponseに新しい型が追加された場合、型チェッカーがエラーを吐く。
/
public static function process(ApiResponse $response): string {
// shapeのキー構造や特定のフィールドによる判別(Tagged Unionパターン)
// Hackのmatch式は、網羅性(Exhaustiveness)を静的に強制する。
// ※ 実際の判別には形状やタグを使うのが定石だが、ここでは簡潔に型そのものでマッチさせる例を示す
return match (true) {
/ HHVMの型ガードとmatch式の組み合わせ /
self::isSuccess($response) => self::handleSuccess($response),
self::isError($response) => self::handleError($response),
self::isRateLimit($response) => self::handleRateLimit($response),
};
}
private static function isSuccess(ApiResponse $r): bool {
// 実際の実装では形状のキー存在チェックなどを行う
return shapes::idx($r, ‘status’) !== null;
}
private static function isError(ApiResponse $r): bool {
return shapes::idx($r, ‘code’) !== null;
}
private static function isRateLimit(ApiResponse $r): bool {
return shapes::idx($r, ‘retry_after’) !== null;
}
private static function handleSuccess(SuccessResponse $r): string {
return “Success: ” . $r[‘status’];
}
private static function handleError(ErrorResponse $r): string {
return “Error [{$r[‘code’]}]: {$r[‘message’]}”;
}
private static function handleRateLimit(RateLimitResponse $r): string {
return “Rate limited. Retry after {$r[‘retry_after’]}s.”;
}
}
なぜこの設計が美しいのか?
1. 暗黙のバグの根絶: 将来、`MaintenanceResponse` という新しい型が `ApiResponse` に追加されたとする。もし従来の `if-else` や網羅性のない分岐であれば、この変更は見落とされ、本番環境で予期せぬフォールバックに落ちていただろう。しかし、適切に設計されたパターンマッチングでは、型チェッカーが「おっと、このパターンが処理されていないぞ」とビルドを止めてくれる。
2. Cognitive Load(認知負荷)の軽減: コードレビュー時、開発者は「すべてのケースを網羅しているか」を目視で確認する必要がない。型チェッカーがそれを証明しているからだ。
—
3. パフォーマンス上の注意点:ガードのコストとJITの効能
「型チェックを細かく行うと、実行時パフォーマンスに影響するのではないか?」という懸念を持つエンジニアもいる。特にPHP出身者に多い勘違いだ。
しかし、HHVMのアーキテクチャにおいて、Strict Mode下的確に型が絞り込まれたコードは、以下のような恩恵を受ける。
- ボックス化(Boxing)の回避: 型が曖昧(mixedやdynamic)な場合、HHVMは値の型タグを保持するためにメモリ上で「ボックス化」を行い、ポインタ経由でアクセスするためキャッシュ効率が落ちる。Union Typesであっても、明示的な型ガード(`is` や判別関数)によって型が確定した瞬間、JITはアンボックス化されたプリミティブ型としてレジスタに直接割り当てることができる。
- 無駄な分岐の削減: 不完全な `else` 句を持つコードは、JITの予測分岐(Branch Prediction)を乱す。すべてのケースが網羅された `match` やガードは、最適化されたジャンプテーブルへとコンパイルされるため、実行速度は圧倒的に高速になる。
—
チーフアーキテクトからの最終通告
コードは単に動けばいいというものではない。特に大規模なWebアプリケーションや高スループットな非同期APIを支える基盤において、型システムは「開発者のうっかり」を防ぐ最後の防壁だ。
Union Typesを扱う際は、常に自問自答しろ。
「もし明日、新しいデータ構造が追加された時、このコードは静的にエラーを返してくれるか?」
答えがノーであるならば、そのコードはリファクタリングの対象だ。Hackの厳格な世界をフルに活用し、美しく、堅牢で、秒速で動作するコードベースを築き上げろ。レビューはそれからだ。