序:数百万行のモノレポを支えるHack型チェッカーの生態系
コードレビューをしていて、こう感じることはないか?
「おい、この小さな修正になぜ型チェッカー(`hh_client`)が3秒も沈黙するんだ?」
数百万行を超えるHackの巨大モノレポにおいて、開発体験(DX)のボトルネックは常に「型チェックの遅延」だ。HHVM(HipHop Virtual Machine)のJITコンパイルがどれほど高速であっても、開発者のエディタ上で動くインクリメンタル・タイプチェッカー(Incremental Type Checker)が悲鳴を上げていれば、チームの生産性は地に落ちる。
我々はHackの厳格な静力学(Strict Mode)の恩恵を最大限に受けている。しかし、その強力な型システムは、依存関係の網の目が複雑化するにつれて、インクリメンタルなグラフ走査のコストを劇的に増大させる。
本稿では、HHVMが内部で保持する依存グラフの挙動を解き明かし、大規模モノレポで型チェック時間を極限まで削るためのアーキテクチャ設計とコードパターンを、チーフアーキテクトの視点から容赦なく叩き込む。
—
1. HHVMインクリメンタル型チェッカーの内部挙動と「死の依存グラフ」
まず、敵を知れ。`hh_server` はデーモンとして常駐し、ファイル変更検知(FileSystem Watcher)をトリガーにインクリメンタルな型チェックを実行する。
この時、内部で何が起きているか?
HHVMは、ファイル単位ではなく、シンボル(クラス、関数、型エイリアス)単位の依存グラフ(Dependency Graph)をメモリ上に構築している。
[File A: BaseClass] —> depends on —> [File B: DerivedClass]
^
| (変更波及: Re-check Cascade)
[File C: Unrelated Service] (影響を受けないはずが…)
なぜ「死の依存グラフ」が生まれるのか?
1. ワイルドカード的なインポートや巨大なユーティリティクラス: 全てのドメイン知識が詰まった `Utils.hack` のようなGodクラスに依存が集中すると、そのファイルを1文字変えただけで、モノレポ全体の数万ファイルが「汚染(Invalidation)」され、フルビルド並みの再計算が走る。
2. 不適切な型エイリアスの乱用: 複雑なジェネリクスや共用体(Union)を深すぎるネストで型エイリアс化すると、型チェッカーの推論エンジン(Inference Engine)がAST(抽象構文木)の展開地獄に陥る。
型チェックを高速化する鉄則はただ一つ。「依存の方向性を一方向に絞り、変更の波及範囲(Blast Radius)を数学的に最小化すること」だ。
—
2. 厳格なStrict Modeと「型依存の局所化」設計パターン
Hackのファイルは必ず `<
ここからは、実務の現場ですぐに応用できる、依存を分離した堅牢なコンポーネント設計のプロダクションコードを示す。
実装例:依存波及を最小化するサービス層の設計
例えば、ユーザーの権限検証とデータ取得を行うAPIハンドラを考えてみよう。悪しき設計では、データベーススキーマ、ビジネスロジック、HTTPレスポンスの型が1つのファイルに結合している。
これを、「DTO(Data Transfer Object)」「インターフェース」「実態(Implementation)」に綺麗に分離し、型チェッカーのキャッシュ効率を最大化する。
<
namespace Acme\UserManagement;
/
- 1. DTO層: プリミティブなデータ構造のみを定義。
- ここへの変更は、ビジネスロジック層に波及させない。
<<__Rx, __NoDynamic>>
class UserDto {
public function __construct(
public int64 $id,
public string $email,
public bool $isActive,
) {}
}
/
- 2. インターフェース層: 具象クラスへの依存を断つ。
/
interface IUserRepository {
public function findById(int64 $id)[]: Awaitable;
}
/
- 3. ドメイン・インフラストラクチャ層: 具象実装。
- HHVMの型チェッカーは、インターフェースさえ変わらなければ
- このクラス内部の実装変更を上位層へ伝播させない(スキップ可能)。
/
final class SqlUserRepository implements IUserRepository {
public function __construct(
private \AsyncMysqlConnection $dbConn,
) {}
public async function findById(int64 $id)[]: Awaitable {
// 実際には非同期クエリを実行
// ここでは簡略化のためモック的な構造を示す
$result = await $this->dbConn->queryAsync(
\Str\format(“SELECT id, email, is_active FROM users WHERE id = %d”, $id)
);
$row = $result->mapRow();
if ($row === null) {
return null;
}
return new UserDto(
(int64)$row[‘id’],
(string)$row[‘email’],
(bool)$row[‘is_active’],
);
}
}
この設計が型チェッカーを高速化する理由
- `SqlUserRepository` の内部ロジック(SQL文の微修正など)を変更しても、`IUserRepository` インターフェースのシグネチャ(メソッド名、引数型、返り値型)が不変であれば、HHVMの型チェッカーは上位の依存ファイルを再チェックする必要がないと即座に判断(Short-circuit)する。
- これにより、インクリメンタルチェックの範囲が数ファイルに限定され、チェック時間が数百ミリ秒単位で維持される。
—
3. パフォーマンス上の罠:避けるべきアンチパターン
コードレビューでよく見かける、型チェッカーを遅延させる「やってはいけない記述」を挙げておく。
① `mixed` や `dynamic` の安易な逃げ
「型付けが面倒だから」と `mixed` や `dynamic` を使うと、HHVMは静的な依存関係を追跡できなくなり、最悪の場合、推論のフォールバックとしてグローバルな再スキャンが発生する。Strict Modeではこれらはコンパイルエラーになるべきだが、ジェネリクス周辺での曖昧な型制約には特に注意が必要だ。
② 巨大な複合ジェネリクス(Generics Hell)
深すぎるジェネリクスの制約は、型チェッカーの単一化(Unification)アルゴリズムに指数関数的な負荷をかける。
// 【非効率】チェッカーを殺す深すぎる型エイリアス
type DeepNestedMap
複雑な型は適度にインターフェースでラップし、チェッカーが推論すべきASTの深さを浅く保て。
—
4. チーフアーキテクトからの提言:CI/CDとローカル環境の同期
ローカルでの `hh_client` の挙動を爆速に保つためには、開発者のマシンパワー頼みではなく、プロジェクト全体のアーキテクチャ規律が必要だ。
1. `.hhconfig` の最適化: 自動インポートや不要なオートロードパスを排除し、型チェッカーの監視対象を最小限に絞る。
2. モジュール境界の厳格化: 将来的な Hack Modules 機能(名前空間を超えたカプセル化)を見据え、コンポーネント間の結合度を常に `.hhconfig` や静的解析ツールで監視する。
型の厳格さは、コードの安全性だけでなく、開発スピードそのものだ。依存関係の迷子をなくし、HHVMが愛す美しい単方向グラフを構築せよ。あなたの書くそのクリーンなコードが、チーム全体の生産性を守る盾となる。