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

こんにちは!日々のHack言語での開発、本当にお疲れ様です。HHVMの圧倒的な実行速度や、厳格な静的型システムが生み出す堅牢なコードベースに魅力を感じている頃ではないでしょうか。

さて、他のオブジェクト指向言語からHackの世界に入ってきた開発者が、必ずと言っていいほど最初に直面する「深遠なる壁」があります。それが今回テーマにするジェネリクスの変位指定(Variance:共変・反変)です。

「なぜこの親クラスの型を受け取る関数に、子クラスのジェネリクスを渡せないんだろう?」
「エラーメッセージの意味がさっぱり分からない……」

そんなモヤモヤを抱えていませんか? 大丈夫です。ここをクリアすれば、あなたも立派なHackの型使い。HHVMの型チェッカーが脳内でスイスイ動くようになりますよ。今日は優しく、かつ本質的なところまで丁寧に紐解いていきましょう!

—

そもそも「変位(Variance)」ってなに?

ジェネリクス(例: `Vector` や `Map`)を使うとき、「型パラメータの関係性が、コンテナ自体の型関係にどう影響するか」というルールが存在します。これを変位(Variance)と呼びます。

百聞は一見にしかず。まずは、私たちが普段書くコードで何が起きているのかを見てみましょう。

登場人物の整理

動物園のシステムを作ると仮定します。

  • `Animal` (すべての動物の親)
  • `Dog` (`Animal` を継承した犬)

ここで、`Animal` を受け取る関数があるとします。

<<__Strict>>
namespace HackMaster;

class Animal {}
class Dog extends Animal {}

function feed_animal(Animal $animal): void {
// どんな動物でもごはんをあげる
}

「犬は動物(`Dog` is a `Animal`)」なのだから、`feed_animal()` に `Dog` を渡せるのは当然ですよね(これは部分型(Subtyping)の基本です)。

では、これがジェネリクス(コンテナ)になった途端にどうなるでしょうか?

—

共変(Covariance):『〜のグループ』は『親のグループ』として扱えるか?

「犬のリスト(`Vector`)は、動物のリスト(`Vector`)として扱えるはずだ!」
直感的にはそう思いますよね。だって犬は動物なのですから。

しかし、Hackの型チェッカーのデフォルトの挙動は、「No」です。これをそのままやろうとすると、型エラーになります。なぜなら、もしそれが無条件に許されると、実行時ではなく静解析の段階で防ぎたい致命的なバグが起きるからです。

共変を有効にするマジックワード `+`

Hackでは、ジェネリクスを受け取る側(または定義する側)で「読み取り専用(Producer)」であることを保証することで、子孫の型への置き換えを許可できます。これが共変(Covariance)です。

記号としては `+` を使います。「プラス=広がる・受け入れる」とイメージすると覚えやすいですね。

<<__Strict>>
namespace HackMaster;

class Animal {}
class Dog extends Animal {}

// 共変インターフェイスの定義
// T の前にある `+` に注目してください!
interface IProducer<<__Covariant> +T> {
public function get(): T;
}

function process_animals(IProducer $animals): void {
// ここで Animal として扱う
}

function run(IProducer $dogs): void {
// 共変(+T)のおかげで、IProducer を IProducer として渡せる!
process_animals($dogs);
}

なぜ共変が必要なのか?(安全性の担保)

もし共変でない場合、犬専用のリーダーを動物全般のリーダーとして使い回せなくなってしまいます。
ただし、共変(`+T`)が許されるのは、その型パラメータ `T` が「返す(Return)」場所(データの生産者)にしか使われていない場合のみです。もし `T` を引数として受け取るメソッド(データの消費者)を作ろうとすると、型チェッカーが即座に怒り出します。「おい、書き込みができる状態だと型安全が崩れるぞ!」と。

—

反変(Contravariance):『親の基準』は『子の現場』で使えるか?

共変とは逆に、今度は反変(Contravariance)を見てみましょう。これは初学者が最も頭を悩ませるポイントです。

記号は `-` を使います。「マイナス」というよりは、矢印の向きが逆転するイメージです。

具体例:犬の調教師と動物の調教師

  • `AnimalTrainer` は「どんな動物でも訓練できる」スキルを持っています。
  • では、特定の「犬だけを可愛がる現場」に、この `AnimalTrainer` を投入できるでしょうか?

答えは「YES」です。動物全般を扱える人は、当然犬も扱えますよね。つまり、要求されるスキル(引数)の範囲が広ければ広いほど、より特化した現場(子クラスの文脈)に放り込むことができるのです。

これをHackのコードで表現してみましょう。

<<__Strict>>
namespace HackMaster;

class Animal {}
class Dog extends Animal {}

// 反変インターフェイスの定義
// T の前にある `-` に注目!
interface IConsumer<<__Contravariant> -T> {
public function consume(T $item): void;
}

function handle_dog_trainer(IConsumer $trainer): void {
// 犬専用の処理
}

function setup_system(IConsumer $animalTrainer): void {
// 反変(-T)のおかげで、IConsumer を IConsumer を要求する場所に渡せる!
handle_dog_trainer($animalTrainer);
}

なぜ反変なのか?

直感とは逆向きに感じるかもしれません。「親の型(`Animal`)を受け取るものが、なぜ子の型(`Dog`)の場所に代入できるのか?」と。
理由はシンプルで、「動物を処理できるなら、犬を渡されてもビクともしないから」です。犬はAnimalの一種ですから、`Animal` を受け取れる関数に `Dog` を放り込んでも、何の問題もなく処理を完遂できます。

反変(`-T`)は、型パラメータが「引数(メソッドの入力)」としてのみ使われる場合に適用されます。

—

ここをクリアすればバッチリ!変位指定のまとめ

変数の世界で迷ったら、次の「黄金律」を思い出してください。

| 変位の種類 | 記号 | データの流れ | 意味 | 覚え方のコツ |
| :— | :— | :— | :— | :— |
| 不変 (Invariant) | なし | 読み書き両方 | 完全一致のみ許す | デフォルト。一番安全で厳格 |
| 共変 (Covariant) | `+T` | 出力のみ (Producer) | `Sub` は `Super` になれる | 「取る」方向(上流へ) |
| 反変 (Contravariant) | `-T` | 入力のみ (Consumer) | `Super` は `Sub` になれる | 「渡す」方向(下流へ) |

  • 共変(`+`):データを「外に出す(返す)」とき。子から親へ昇格できる。
  • 反変(`-`):データを「中に入れる(受け取る)」とき。親から子へ降格(適用)できる。

この原則を理解していれば、HHVMの型チェッカーがエラーを吐いたときも、「あ、ここはデータを書き込んでいるから共変にしちゃダメだな、反変にすべきだな」と、自分で自信を持ってコードを修正できるようになります。

—

おわりに

Hackの厳格な静的型システム(Strict Mode)は、私たちのコードから実行時エラーを根絶してくれる最高のエージェントです。最初は厳しく感じる変位指定のルールも、HHVMのアーキテクチャが「安全なメモリと型の世界」を守るための必然の仕組みなのだと分かれば、頼もしい相棒に見えてくるはずです。

この難所を越えたあなたなら、どんなに複雑なジェネリクスを使ったアーキテクチャでも美しく設計できるでしょう。
さあ、今日も自信を持って、最高に堅牢なHackコードを書き上げていきましょう!

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