【入門編】Hackの『Function Types』における引数と戻り値の共変・反変制約:高階関数の安全な設計 – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

やあ。Hackの世界へようこそ。HHVMの深淵を覗き込み、静的型システムの厳格さに触れようとしている君は、すでに優れたエンジニアの入り口に立っているね。

今日は、Hackの中でも特に「高階関数」を扱う際に避けては通れない、「変位(Variance:共変・反変)」という概念について話そう。ここを理解すると、君の書くコードは「動く」レベルから「堅牢で予測可能」なレベルへと一気に昇華されるよ。

—

1. なぜ「関数型」の型定義で悩むのか?

Hackにおいて、関数を引数に取ったり戻り値にしたりする「高階関数」は日常茶飯事だよね。でも、こんな疑問を持ったことはないかな?

「引数に『動物(Animal)』を受け取る関数を期待している場所に、『犬(Dog)』を受け取れる関数を渡したら……型安全なの?」

直感的には「犬は動物だから大丈夫そう」に思えるよね。でも、実はここには「反変(Contravariance)」と「共変(Covariance)」という、型システムの厳格なルールが隠れているんだ。

2. 図解:引数は「反変」、戻り値は「共変」

この図を頭に焼き付けてほしい。

  • 引数(入力)は「反変(Contravariant)」: 期待される型より「広い(親)」型を受け取れる関数は安全。
  • 戻り値(出力)は「共変(Covariant)」: 期待される型より「狭い(子)」型を返す関数は安全。

なぜこうなるのか?簡単な例を見てみよう。

namespace App;

abstract class Animal {}
class Dog extends Animal {
public function bark(): void { echo “Bow-wow!”; }
}

// Animalを引数に取る関数型
type AnimalHandler = (function(Animal): void);

function process(AnimalHandler $handler): void {
// ここではAnimalなら何でも処理できると期待している
$handler(new Animal());
}

なぜ引数は「反変」なのか?

もしここで `(function(Dog): void)` という関数を渡したらどうなるかな? `process` 関数は `Animal` を渡そうとするのに、受け取り側は `Dog` しか扱えない。これではランダムな動物が来た瞬間にクラッシュするよね。だから、「より広い型(Animal)」を受け取れる関数だけが安全なんだ。

なぜ戻り値は「共変」なのか?

逆に、戻り値は「より具体的な型」を返しても安全だよね。「動物を返せ」と言われて「犬」を返せば、受け取り側は動物として扱えるから何の問題もない。

—

3. 実践:Hackでの高階関数設計

では、実際に型エラーを回避しつつ、柔軟な高階関数を書くための実装例を見てみよう。

<<__EntryPoint>>
function main(): void {
// 正しい実装例
// 引数にAnimalをとる関数を期待する場所に、より広いObjectをとる関数を渡す(反変的アプローチ)
$handler = (mixed $a): void => {
echo “Processing object safely.”;
};

// 型チェッカーはこれを許可する
executeHandler($handler);
}

function executeHandler((function(Animal): void) $callback): void {
$callback(new Dog());
}

陥りやすい罠:型不一致エラー

初心者がよくやる間違いは、「サブタイプを引数に取ろうとしてしまうこと」だ。

// これは型チェッカーに怒られる例
function acceptDogHandler((function(Dog): void) $callback): void {
// Error! (function(Animal): void) は (function(Dog): void) に代入できない
// なぜなら、Dog以外(猫とか)が来たら死んでしまうから
}

このエラーが出たら、「自分はもっと制限の緩い(親クラスを受け取る)関数を渡そうとしていないか?」と自問自答してみてほしい。

—

4. 現場で役立つ「型安全」の知恵

Hackの厳格なモード(`<<__Strict>>`)で開発していると、こうした型制約が開発を大きく助けてくれることに気づくはずだ。

1. 境界を意識する: 関数のインターフェースを定義する際、「この関数はどれだけ広い範囲の入力を許容すべきか?」を考えること。
2. Genericsの活用: もし型が複雑になりすぎるなら、`` のようにジェネリクスを使って、コンパイル時に型の整合性を担保しよう。
3. HHVMの恩恵: HHVMは実行時にこれら型情報をチェックしている。つまり、ここで学んだルールを守れば、実行時の予期せぬ型エラー(`TypeError`)を排除できるんだ。

—

まとめ

  • 引数(入力)は「より広く」: `(function(Animal): void)` を要求されたら、`(function(Object): void)` を渡せ。
  • 戻り値(出力)は「より狭く」: `(function(): Dog)` を返せと言われたら、`(function(): Animal)` と定義してはいけない(逆はOK)。

ここをクリアできれば、君はもうHackの型システムの深淵に触れたと言っていい。最初は難しく感じるかもしれないけれど、一度体得してしまえば、君の書くコードは驚くほど堅牢で、かつ美しいものになるはずだ。

さあ、次のコードブロックで、もっと複雑なジェネリクスに挑戦してみないか? 準備ができたらまたいつでも聞きに来てくれ。応援しているよ。

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