【入門編】Hackの『Contravariance』と『Covariance』の完全理解:ジェネリクス型における代入ルールの正体 – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

こんにちは。Hackの深淵へようこそ。

Hackの静的型システムは、単なる「エラーを防ぐためのガードレール」ではありません。あれは、実行時のメモリ安全性と、複雑なロジックをスケールさせるための「数学的な証明」そのものです。

今日は、多くのエンジニアが直感に反して苦しむ「ジェネリクスの変位(Variance:共変・反変)」という難所に挑みましょう。ここを理解すれば、あなたはただHackを書く人から、Hackを「設計する人」へと進化できますよ。

—

なぜ「List」に「List」を代入できないのか?

まず、直感的な疑問から始めましょう。
「犬(Dog)は動物(Animal)の一種なのだから、`List` は `List` と見なせるはずだ」——そう思いますよね。

しかし、Hackの型チェッカーは容赦なく冷たくエラーを返します。なぜでしょう?

共変(Covariance)の罠:読み込みだけなら安全だが…

もし `List` を `List` として扱えたら、何が起きるか。

// 概念的な例
function feedAnimals(Vector $animals): void {
// ここでCat(猫)を追加できてしまう!
$animals->add(new Cat());
}

$dogs = Vector{new Dog()};
feedAnimals($dogs); // もしこれが許されると…
// $dogsの中身が、DogとCatが混在する「汚染されたリスト」になる!

これが「共変」の危険性です。「読み込み専用」であれば安全ですが、「書き込み(追加)」ができる以上、型安全は崩壊するのです。だからこそ、Hackではデフォルトでジェネリクスは「不変(Invariant)」として扱われます。

—

Hackの「型システムを掌握する」ためのキーワード

ここを整理すれば、もう迷うことはありません。

  • 不変 (Invariant): `Vector` はこれ。`Dog` と `Animal` は別物として扱う。最も安全。
  • 共変 (Covariance):`+T`。読み取り専用。型が広がる方向への代入が可能。
  • 反変 (Contravariance):`-T`。書き込み専用。型が狭まる方向への代入が可能。

1. 共変 (`+T`): データを「取り出す」とき

「動物を扱う箱」から、中身を取り出すだけなら、それは「動物」として扱っても問題ありませんよね。`KeyedIterable` など、イテレータ系インターフェースでよく見かけます。

// +T を使うと、サブタイプをより広い型に代入できる
interface ReadableBox<+T> {
public function get(): T;
}

function process(ReadableBox $box): void {
$animal = $box->get(); // 何が入っていてもAnimalとして扱えるので安全
}

// ReadableBox は ReadableBox のサブタイプになる

2. 反変 (`-T`): データを「消費する」とき

逆に、データを「受け取る」だけのインターフェースなら、逆の代入が可能になります。「動物ならなんでも扱える機械」に「犬専用の機械」を渡すのは、論理的に正しいですよね。

// -T を使うと、スーパタイプをより狭い型に代入できる
interface Consumer<-T> {
public function consume(T $item): void;
}

function run(Consumer $dogConsumer): void {
$dogConsumer->consume(new Dog());
}

// Consumer は Consumer として振る舞える
$animalConsumer = new AnimalConsumer();
run($animalConsumer);

—

現場でエラーに直面したときの処方箋

開発中に `Expected Vector but got Vector` と言われたら、以下の3つをチェックしてください。

1. 「そのジェネリクス、書き込みが必要ですか?」

  • もし読み取り専用なら、`Vector` ではなく `KeyedIterable` のような、共変(`+T`)が定義されたインターフェースへ型を絞り込みましょう。

2. 「型の階層が深すぎませんか?」

  • クラスの継承関係が複雑すぎると、型チェッカーが追いきれず、安全のために厳格な「不変」を要求します。インターフェースを小さく分割(インターフェース分離の原則)することで解決するケースがほとんどです。

3. 「無駄にジェネリクスを使いすぎていませんか?」

  • 無理にジェネリクスで汎用化せず、型パラメータを具体的に固定することで、Hackの静的解析の恩恵を最大限に受けられます。

—

まとめ:型は「制約」ではなく「設計図」

Hackの静的型システムが厳しいのは、あなたのコードが「将来にわたって壊れないこと」を数学的に保証するためです。

  • 取り出すだけなら「共変 (`+T`)」
  • 受け取るだけなら「反変 (`-T`)」
  • 読み書きするなら「不変(ジェネリクスそのままで)」

この三原則を意識するだけで、あなたの書くコードは驚くほど堅牢になります。型エラーは「邪魔な警告」ではありません。設計上の矛盾を教えてくれる「頼もしいパートナー」なのです。

さあ、この理論を武器に、今日も最高に美しいHackコードを書き上げてください。また何かあれば、いつでも聞きに来てくださいね。応援しています!

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