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

こんにちは!HHVMの内部構造やHackの厳格な型システムの底知れぬ魅力に憑りつかれたあなたなら、日々の開発で「おっ、ここで型エラーか……」とニヤリとすることも多いのではないでしょうか。

今回は、Hackの静的型チェッカー(Typechecker)を完全手玉に取るための最難関トピック、「ジェネリクスにおける共変(Covariance)と反変(Contravariance)」の世界へあなたを誘います。

他の言語(JavaのワイルドカードやC#のout/inキーワードなど)で挫折した人でも大丈夫。ここをクリアすれば、Hackの型システムの本質が手に取るように分かり、複雑なアーキテクチャでも迷わず安全なコードが書けるようになりますよ。

—

1. なぜジェネリクスの代入で頭が痛くなるのか?

まずは、次のコードを見てください。あなたはスーパークラス `Animal` と、それを継承したサブクラス `Dog` を持っています。

<<__Strict>>
namespace HackMaster;

class Animal {}
class Dog extends Animal {}

function processAnimals(Vector $animals): void {
// 何らかの処理
}

ここで、`Vector` 型のインスタンスを `processAnimals()` に渡そうとしたとき、Hackの型チェッカーは容赦なくエラーを吐き出します。

> “Expected `Vector`, got `Vector`”

「あれ? `Dog` は `Animal` の一種なんだから、`Vector` も `Vector` の代わりになるはずでは?」と思いましたよね?
実はここに、ジェネリクスと継承が交わるときに発生する「型の罠」が潜んでいます。

—

2. そもそも「変性(Variance)」ってなに?

変性(Variance)とは、「ある型同士の継承関係(親子関係)が、それらを包むジェネリック型(例:`Container`)の間でどう引き継がれるか」を定めたルールのことです。

Hack(および多くの静的型付け言語)では、デフォルトのジェネリクスは「非変(Invariant)」です。つまり、`Dog extends Animal` であっても、`Container` と `Container` には親子関係は一切ないものとして扱われます。

なぜそんなに冷酷なルールになっているのでしょうか? その理由を、次のコードでシミュレーションしてみましょう。

// 【架空の危険な世界線】もし Vector を Vector に代入できたら…?
<<__Strict>>
namespace HackMaster;

function addCatToAnimals(Vector $animals): void {
// Animal の仲間なら、猫(Cat)を追加しても安全なはず!
$animals[] = new Cat();
}

class Cat extends Animal {}

もし `Vector` が `Vector` として扱えてしまったら、上記の関数に `Vector` を渡した瞬間、犬のコンテナの中に猫が混入するという大惨事(Type Safetyの崩壊)が起きてしまいます。HHVMのJITコンパイラもこれには泣き出します。

だからこそ、デフォルトではジェネリクスは非変(厳密に一致しなければならない)なのです。

—

3. 救世主「共変(Covariance: `+`)」:読むだけの世界

「でも、データを読み取るだけなら、`Dog` のコンテナを `Animal` のコンテナとして扱っても安全なはずだよね?」

その通りです。その「読み取り専用(Producer)」の文脈において、親子関係の向きをそのまま維持することを共変(Covariance)と呼びます。

Hackでは、ジェネリックパラメータの前に `+` をつけることで、その型が「共変」であることを型チェッカーに教えられます。

<<__Strict>>
namespace HackMaster;

class Animal {}
class Dog extends Animal {}

// 共変インターフェース(データの「生産者」)
interface IProducer<<__Covariant T>> {
public function get(): T;
}

function printAnimalName(IProducer $producer): void {
// ここで Animal として扱える
}

<<__EntryPoint>>
function main(): void {
// IProducer を IProducer の代わりに渡せる!これが共変の威力
// $dogProducer = …;
// printAnimalName($dogProducer);
}

  • イメージ: `Dog` は `Animal` なので、`IProducer` は `IProducer` の「子」とみなせる(方向が一致=共変)。
  • 制限: 共変に指定した型パラメータ(`T`)は、メソッドの返り値の型としてのみ使用できます。引数に使うことは許されません(前述の「猫の混入」を防ぐためです)。

—

4. もう一つの顔「反変(Contravariance: `-`)」:受け入れる世界

共変があればその逆もあります。それが反変(Contravariance)です。
こちらは、データの書き込み・消費(Consumer)を行う場合に真価を発揮します。

Hackでは、ジェネリックパラメータの前に `-` をつけて表現します。

<<__Strict>>
namespace HackMaster;

// 反変インターフェース(データの「消費者」)
interface IConsumer<<__Contravariant T>> {
public function consume(T $value): void;
}

function handleDogConsumer(IConsumer $consumer): void {
// …
}

<<__EntryPoint>>
function testContravariance(): void {
// 驚くべきことに、IConsumer は IConsumer の代わりとして渡せる!
// なぜなら、「Animalを受け取れる仕組み」があれば、「Dogを受け取れ」と言われても余裕で対処できるから。
// $animalConsumer = new AnimalConsumer();
// handleDogConsumer($animalConsumer);
}

  • イメージ: `Dog extends Animal` なのに、コンテナの代入関係は逆転して `IConsumer` が `IConsumer` の代わりになる(方向が逆=反変)。
  • 制限: 反変に指定した型パラメータ(`T`)は、メソッドの引数の型としてのみ使用できます。

—

5. まとめ:ここをクリアすればHackの型マスター!

変性のルールを、HHVMの厳格な世界観に合わせて一枚の表にまとめました。

| 変性の種類 | 記号 | 意味・方向 | メソッド内での使用許可 | ユースケースのイメージ |
| :— | :— | :— | :— | :— |
| 非変 (Invariant) | なし | `A` と `B` に親子関係なし | 引数・返り値どちらもOK | 読み書き両方行うコレクション (`Vector`, `Map`) |
| 共変 (Covariant) | `+` (`<<__Covariant>>`) | `A` が子なら `C` も子 (`A` = `C`) | 返り値のみ | データの生産者・イテレータ (`Iterator<+T>`) |
| 反変 (Contravariant) | `-` (`<<__Contravariant>>`) | `A` が子なら `C
` は親 (`A` ≠ `C`) | 引数のみ | コールバック・コンシューマ (`Consumer<-T>`) |

複雑なフレームワークの設計や、独自のドメインモデルでジェネリクスを巧みに使いこなすとき、この共変・反変の知識があなたを強力にサポートしてくれます。「型チェッカーに怒られない美しい設計」がスラスラ書けるようになると、Hackコーディングが何倍 aussi 楽しくなりますよ!

ここをクリアしたあなたなら、もうHackの基本で躓くことはありません。自信を持って、より高みを目指していきましょう!

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