【テクニカル・上級編】HHVM型チェッカーの『Incremental Check』を最大限活用するコード分割のベストプラクティス – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

HHVM型チェッカーの『Incremental Check』を極限まで引き出す:大規模Hackコードベースの依存トポロジー設計

我々は日々、数百万行に及ぶHackのコードベースをミリ秒単位のフィードバックループで回す戦いに身を投じている。
HHVMの型チェッカー(`hh_client` / `hh_server`)は、OCamlの血統を受け継ぐ怪物的な速度を誇るが、プロジェクトがスケールするにつれて、そのインクリメンタル・チェックの挙動に頭を悩ませるシニアエンジニアは少なくない。

「なぜ、たった1行の変更で数秒の待機が発生するのか?」
「なぜ、無関係なモジュールまで再評価の連鎖に巻き込まれるのか?」

答えはシンプルだ。君の書いたコードの依存関係(Dependency Graph)が、型チェッカーのインクリメンタル・エンジンにとっての“最悪の形状”をしているからだ。

今回は、HHVMの型チェッカーが内部でどのようにファイル間の依存関係を追跡し、メモリ上のグラフを構築しているのかという低レイヤのメカニズムを解き明かし、インクリメンタル・チェックの恩恵を100%引き出すためのモジュール設計の極意を授けよう。

—

1. HHVM型チェッカーとインクリメンタル・エンジンの内部メカニズム

型チェッカーのデーモン(`hh_server`)は、起動時に全ファイルのAST(抽象構文木)をパースし、シンボルテーブルと依存関係の有向グラフ(Directed Graph)をメモリ上に構築する。

依存関係の粒度と「Lazy Resolution」の罠

多くのエンジニアは、ファイル単位で依存関係が管理されていると思っている。だが、それは幻想だ。
型チェッカーは、シンボル(クラス、関数、タイプエイリアス、グローバル定数)単位で依存関係をトラッキングしている。

あるファイル `A.hack` の中で `B::foo()` を呼び出している場合、グラフ上では `A.hack` 全体が `B.hack` に依存するのではなく、「`A.hack` の特定の式」が「`B::foo` というシンボルの型シグネチャ」に依存する。

ここで問題になるのが 「型シグネチャの変更」と「実装の変更」の区別 だ。

  • 実装(Body)の変更: メソッドの中身を変えただけの場合、型チェッカーは依存先を再チェックする必要がない。
  • シグネチャ(Signature)の変更: パラメータの型、戻り値の型、可視性、あるいはクラスの継承関係を変更した場合、そのシンボルに依存するすべての下流ファイル(Downstream files)が無効化(Invalidated)され、再評価のキューに積まれる。

この再評価の伝播(Invalidation Cascade)こそが、大規模コードベースでビルドを遅延させる元凶なのだ。

—

2. 依存関係の「密結合地獄」を回避するモジュール設計

インクリメンタル・チェックを殺す最大のアンチパターンは、「全知全能のユーティリティクラス」や「循環依存に近い巨大なドメインモデル」の構築である。

以下のコードを見てほしい。これは典型的な「最悪の依存トポロジー」を生み出す設計だ。

❌ アンチパターン:全ドメインが結合した God Class

// @strict
namespace Hack\Engine\AntiPattern;

// 変更頻度が高く、かつシステム全体から参照される危険なクラス
final class SystemContext {
private static ?SystemContext $instance = null;

public function __construct(
public UserSession $session,
public DatabaseConnection $db,
public FeatureFlags $flags,
) {}

// このシグネチャを変更すると、システム内の全コントローラーとサービスが再チェックされる
public static function getCurrent(): this {
invariant(self::$instance !== null, “Context not initialized”);
return self::$instance;
}
}

この `SystemContext` を少しでも変更しようものなら(例えば `FeatureFlags` の型を変えたり、新しいプロパティを追加したり)、それに依存する数千のファイルが一斉に無効化される。`hh_server` は数秒間にわたってCPUコアをフル回転させ、インクリメンタル・チェックのメリットは完全に相殺される。

—

3. 依存の方向を制御する「Interface Injection」と「Dependency Inversion」

インクリメンタル・チェックを極限まで高速に保つための鉄則は、「変更されにくいコアな抽象」から「変更されやすい具象」へ依存を向けること、そして「型シグネチャの変更の影響範囲を局所化する」ことだ。

✅ ベストプラクティス:インターフェースによるシグネチャの隔離

ドメインロジックの結びつきを断ち切るために、不変のインターフェース層を挟む。

// @strict
namespace Hack\Engine\BestPractice;

/

  • 変更が極めて稀な「抽象」の定義。
  • このファイルのシグネチャが安定していれば、下流への影響はゼロになる。

/
interface IReadonlySession {
public function getUserId(): int;
public function hasPermission(string $permission): bool;
}

final class UserSession implements IReadonlySession {
public function __construct(private int $userId, private Set $perms) {}

public function getUserId(): int {
return $this->userId;
}

public function hasPermission(string $permission): bool {
return $this->perms->contains($permission);
}
}

ここで重要なのは、コンシューマー側(サービス層)が具象クラス `UserSession` ではなく、抽象インターフェース `IReadonlySession` に依存するように型ヒントを絞ることだ。

// @strict
namespace Hack\Engine\BestPractice;

final class OrderProcessor {
// 具象ではなく、安定したインターフェースに依存させる
public function __construct(private IReadonlySession $session) {}

public function process(Order $order): void {
if (!$this->session->hasPermission(‘checkout’)) {
throw new UnauthorizedException();
}
// 処理ロジック…
}
}

この設計により、`UserSession` の内部実装やプライベートプロパティを追加・変更しても、`IReadonlySession` のシグネチャ自体が変わらない限り、`OrderProcessor` を含む他のファイルは型チェッカーによって「変更なし」とみなされ、再評価スキップの対象(Cache Hit)となる。

—

4. 厳格な型(Strict Mode)とTypecheckerのメモリ・CPU最適化

Hackの `// @strict` モードは、型推論の曖昧さを排除し、チェッカーのアルゴリズムを最も効率的なパスに誘導する。
だが、動的な性質を持つコードや、以下のような構造はインクリメンタル・チェッカーに無駄な計算量を強いる。

1. 巨大な Type Alias の乱用とネスト

複雑にネストされた `shape` や `type` の定義を別ファイルに散在させ、かつ相互に参照させると、型チェッカーは型の展開(Type Expansion)のためにグラフの深部を何度も再計算する。
型エイリアスは可能な限りフラットに保ち、ドメインごとに独立したファイルへ閉じ込めよ。

2. Genericsの過度な複雑化

高度なジェネリクス制約(`where` 句など)は強力だが、依存グラフの辺(Edges)を爆発的に増やす原因になる。

// 危険な例:複雑すぎる制約は型チェッカーの推論コストを跳ね上げる
class Repository where T1 as T2, T2 as IEntity {
// …
}

インクリメンタル・チェックを高速化したいホットパスでは、ジェネリクスの境界を必要最小限に抑え、具象型やシンプルなインターフェースで代替することを検討すべきだ。

—

5. 現場で実践すべきチェックリスト

1. 「God File」の解体:
1ファイルに複数の主要クラスや、大量のグローバル関数を詰め込むな。1ファイル1クラス(または密結合した小規模な関連クラス群)の原則を徹底し、依存の粒度を最小化せよ。
2. シグネチャの変更コストを意識する:
公開メソッドの引数や戻り値の型を変更する際、「この変更は何ファイルに伝播するか?」を `hh_client –show-id` や依存グラフツールで常に意識せよ。
3. Traitの乱用に注意:
Traitは強力だが、コードベース全体に暗黙の依存関係をばら撒く。Trait内のメソッドシグネチャを変更すると、それをuseしている全クラスが無効化されるため、インクリメンタル・チェックの効率が著しく低下する。Traitは「振る舞いの水平方向の共有」にのみ限定し、ドメインの結合には使うな。

—

結びにかえて

HHVMの型チェッカーは、正しく飼い慣らせば、数万ファイルのコードベースであっても一瞬で型安全性を担保してくれる世界最高峰のエンジンである。
しかし、開発者が無秩序な依存関係(Spaghetti Dependencies)を放置すれば、インクリメンタル・エンジンは無駄な再計算の渦に飲み込まれ、その真価を発揮できなくなる。

コードを書くときは、単に「動くもの」を作るな。
「型チェッカーのメモリ上のグラフが、どう変化し、どう伝播するか」を脳内でコンパイルしながらコードを配置しろ。

それこそが、大規模Hackエコシステムを支配するシニアエンジニアの流儀である。

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