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

ジェネリクスの深淵へ:Hackにおける「変位(Variance)」の正体と型安全性の極意

こんにちは。HHVMの深淵を覗き込み、Hackの型システムと日々対話している開発者です。

Hackを触り始めると、必ず一度は「なぜこの型は代入できないのか?」という壁にぶつかりますよね。特にジェネリクス(Generics)を使った際、`Vector` を `Vector` に代入しようとして、型チェッカーに怒られた経験はありませんか?

実はこれ、Hackの静的型システムが最も厳格に守ろうとしている「変位(Variance)」というルールが働いているからなんです。今日は、この一見難解な概念を、コードの裏側にある「型安全性」という観点から紐解いていきましょう。

—

1. なぜ「猫のリスト」は「動物のリスト」になれないのか?

まずは、直感的に「おかしいな?」と思うコードを見てみましょう。

abstract class Animal {}
class Cat extends Animal {}
class Dog extends Animal {}

function processAnimals(Vector $animals): void {
// ここでDogを追加できてしまうと…
$animals->add(new Dog());
}

<<__EntryPoint>>
function main(): void {
$cats = Vector { new Cat() };

// もしこれが許されると…
// processAnimals(cats);

// $catsの中にDogが混入し、実行時にクラッシュする!
}

もし `Vector` が `Vector` として扱えてしまうと、`processAnimals` 関数の中で `Dog` を追加できてしまいますよね。すると、`$cats` という「猫しか入っていないはずの箱」の中に「犬」が紛れ込むことになります。

これが「共変(Covariance)」を安易に許してはいけない理由です。 Hackの厳格な型システムは、メモリの安全性を守るために、この「型の崩壊」をコンパイル時に完全に遮断しているのです。

—

2. 変位(Variance)の3つの顔

変位とは、ジェネリクス型の「サブタイプ関係」を、元の型とどう関連付けるかを決めるルールです。

① 不変(Invariant):デフォルトの挙動

Hackの `Vector` などは「不変」です。`Cat` が `Animal` のサブタイプであっても、`Vector` は `Vector` のサブタイプにはなりません。

  • ルール: 「読み込み」も「書き込み」も両方行うなら、型は完全に一致していなければならない。

② 共変(Covariance):読み取り専用の特権

「読み取るだけ」なら、子クラスを親クラスとして扱っても安全ですよね?これを実現するのが `+` 記号です(Hackではインターフェース等で利用可能です)。

interface Readable<+T> {
public function get(): T;
}

`+T` と書くことで、「この型は `T` のサブタイプとして扱っても安全(読み込みのみだから)」と型チェッカーに教えることができます。

③ 反変(Contravariance):書き込み専用の逆転発想

これは少し直感的ではありません。`Consumer` を `Consumer` として使うケースです。

interface Consumer<-T> {
public function consume(T $item): void;
}

`Animal` を受け取れる処理能力があるなら、`Cat` を渡されても処理できますよね。つまり、「親クラスを受け取れるなら、子クラスも受け取れる」。これが反変 `-T` です。

—

3. 現場で陥りやすい「型エラー」と解決策

よくあるのが、`Vector` や `Map` を関数の引数として渡すときに発生する型エラーです。

NGパターン:

function takeContainer(Vector $v): void { … }

$cats = Vector { new Cat() };
takeContainer($cats); // Error: Vector は Vector ではない

解決策1:読み取り専用なら `KeyedIterable` を使う
もし関数内で要素の追加(`add`)を行わないなら、`Iterable` や `KeyedIterable` を使いましょう。これらは共変的に振る舞えるよう設計されています。

// Iterable<+T> なので、Vector を受け取れる
function readOnlyProcess(KeyedIterable $animals): void {
foreach ($animals as $animal) { … }
}

解決策2:どうしても変更が必要ならジェネリクスを使う
関数自体をジェネリックにして、型の関係を柔軟にします。

function addAnimal(Vector $v): void {
$v->add(new Cat());
}

`super` を使うことで、「`Cat` またはその親クラスのリストなら受け入れる」という柔軟な制約を設けることができます。

—

最後に:型システムは「敵」ではなく「盾」

Hackの型チェッカーが厳しいのは、あなたのコードを批判するためではなく、「実行時に発生する予期せぬエラー」という名の地雷からあなたを守るためです。

共変・反変を理解すると、「なぜこの型は合わないのか」が論理的に説明できるようになります。この感覚が身につけば、あなたはもうHackの初級者を卒業し、堅牢なアーキテクチャを設計できる中級者への道を歩んでいると言えるでしょう。

「型が合わない」と悩んだときは、一度立ち止まって「この型は読み込んでいるのか?書き込んでいるのか?それとも両方か?」と自分に問いかけてみてください。その答えが、必ずコードを正しい方向へ導いてくれますよ。

また何か壁にぶつかったら、いつでも聞きに来てくださいね。Hackの深淵はまだまだ広いですから。

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