HHVM型チェッカーの極限最適化:Incremental Checkを支配するコード分割アーキテクチャ
コードレビューをしていると、数百万行規模のHackコードベースにおいて「型チェック(`hh_client`)に数十秒、ひどい時は数分かかる」という悲鳴を耳にする。CI/CDパイプラインは詰まり、ローカルでの開発フィードバックループは死に、エンジニアの生産性は音を立てて崩れていく。
なぜ、あなたの書いたコードは型チェッカーを鈍らせるのか?
そして、どうすればHHVMの型チェッカー(Hack Typechecker)の心臓部であるIncremental Check(差分コンパイル・解析)を極限まで高速化できるのか。
今回は、HHVMの型システムとメモリ空間の挙動を知り尽くしたアーキテクチャの視点から、コンポーネント設計と依存関係の黄金律を叩き込む。
—
1. なぜ型チェッカーは「遅く」なるのか?(HHVMの内部挙動)
Hackの型チェッカーは、デーモンプロセスとして常駐し、ファイル変更検知(FileSystem Watcher)からAST(抽象構文木)の差分を計算してインメモリの型グラフを更新する。
ここでボトルネックになるのは、「推移的依存関係(Transitive Dependencies)の連鎖爆発」だ。
[God Class A] —> [Service B] —> [DTO C] —> [Value Object D]
もし、最下層の `Value Object D` の型を1文字変更したとする。型チェッカーは、`D` に依存するすべての親ノード(`C`, `B`, `A`)の整合性を再検証しなければならない。これが「巨大なモジュール結合」を生んでいるコードベースで起きた場合、変更ファイルは1つであっても、実質的に全ファイルの数パーセント、あるいは全グラフの再スキャンが走り、Incremental Checkの恩恵が完全に失われる。
型チェッカーを高速化する3つの鉄則
1. 結合度の方向を単一化する(循環依存の完全排除)
2. 「巨大なインターフェース」を分割し、依存のスコープを最小化する
3. ジェネリクス(Generics)と抽象化の境界を適切に引き、型の推論爆発を防ぐ
—
2. 実務で即効性を発揮する「モジュール分割」の設計パターン
では、具体的にどうコードを分割すべきか。
プロダクション環境を想定し、ドメインロジック、データ転送オブジェクト(DTO)、そして外部API連携が綺麗に分離され、Incremental Checkのグラフを最小化する設計パターンをコードで示しよう。
すべて `hh_strict` モードで記述する。
悪い例:すべてが結合したアンチパターン
// @lint-ignore-all
<
namespace Vendor\App\AntiPattern;
// 1つのファイルにあらゆる責務が詰め込まれ、DTOの変更がドメイン全体を再検証させる
class MonolithService {
public function execute(array
// 処理…
}
}
このようなコードは、少しの変更でHHVM全体の型グラフが再構築されるため、Incremental Checkのキャッシュが無効化される。
—
良い例:厳格な型境界とインターフェース分離によるプロダクションコード
以下のコードは、依存関係を極限まで絞り込み、型チェッカーが「差分のみ」を瞬時に検知できるように設計されたモジュールの模範解答である。
<
namespace Vendor\App\Core;
/
- 読み取り専用のデータ転送コントラクト(Immutable DTO)
- 変更頻度が低い基底レイヤーに配置する。
/
<<__ConsistentConstruct>>
abstract class BaseDTO {
public abstract function toArray(): dict
}
/
- ユーザー情報を表す不変オブジェクト
/
final class UserDTO extends BaseDTO {
public function __construct(
public readonly int $id,
public readonly string $email,
) {}
public function toArray(): dict
return dict[‘id’ => $this->id, ‘email’ => $this->email];
}
}
次に、ビジネスロジックを担うサービス層だ。ここでは具象クラスではなく、最小限のインターフェースに依存させることで、型チェッカーの依存グラフをスリムに保つ。
<
namespace Vendor\App\Service;
use namespace Vendor\App\Core;
/
- リポジトリの抽象化
- インターフェースを分離することで、実装側の変更が上位レイヤーの型チェックに影響を与えないようにする。
/
interface IUserRepository {
public function findById(int $id): ?Core\UserDTO;
}
/
- ユーザー処理ドメインサービス
/
final class UserProcessor {
// 具象ではなくインターフェースに依存させることで、結合度を下げ、
// 型チェッカーの依存関係トラバーサルコストを劇的に削減する。
public function __construct(
private IUserRepository $repository,
) {}
public function processUser(int $id): string {
$user = $this->repository->findById($id);
if ($user === null) {
throw new \InvalidArgumentException(“User not found: {$id}”);
}
// 厳格な型システムにより、ここで余計なキャストやチェックは不要
return \Str\format(“Processed user: %s (ID: %d)”, $user->email, $user->id);
}
}
—
3. パフォーマンス上の注意点:型チェッカーを泣かせないためのHack特有の知見
1. `mixed` や動的呼び出しの排除
`mixed` や `dynamic` 型をコードの境界線(DTOやAPIレスポンスのデシリアライズ層)以外で使うと、型チェッカーは安全な推論を諦め、広範囲な影響分析を行うためIncremental Checkの効率が落ちる。型境界でのみパースを行い、内部は常に `strict` な具象型で貫通させよ。
2. 大きなファイル(God File)の分割
1ファイルに複数のクラスや複雑なジェネリクス関数を詰め込むと、そのファイル内の1行を変えただけで、HHVMはそのファイル全体(場合によっては数千行)のASTを再パース・再型チェックする。「1ファイル1クラス(または密結合な小規模ペア)」の原則を徹底せよ。
3. ジェネリクス(Generics)の制約(Constraints)の最適化
複雑すぎる `where` 制約や、深すぎる総称型のネストは、型チェッカーの型推論エンジン(Typing Engine)に重い負荷をかける。推論のパスが長くなればなるほど、Incremental Checkの差分計算アルゴリズムは遅延する。
—
テクニカルリードからの総括
コードの美しさは、単に「バグがないこと」や「読みやすいこと」だけを指すのではない。「開発フィードバックループの速度を殺さない構造になっているか」も、アーキテクチャの極めて重要な評価指標だ。
あなたが何気なく追加した「不要なモジュール間の結合」や「巨大な共通ファイルの肥大化」が、チーム全体のビルド・型チェック時間を着実に蝕んでいる。
今日のレビューから意識してほしい。
- 「この変更は、どのファイルの型再検証を引き起こすか?」
- 「依存の方向は単一に流れているか?」
型チェッカーとHHVMのアーキテクチャに敬意を払った美しいモジュール分割こそが、スケールするHackアプリケーションの唯一の生命線である。