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

こんにちは!Hack言語の世界へようこそ。フルスタックエンジニアの先輩として、今日から君をさらに一段上のレベルへ導く特別なトピックを用意しました。

私たちが日々向き合っているHack言語は、PHPの系譜を持ちながらも、妥協のない厳格な静的型システム(Strict Mode)と、HHVM(HipHop Virtual Machine)による圧倒的な高速実行を武器に進化を続けています。

そのHackの真骨頂とも言えるのが、「関数型(Function Types)」における変性(Variance:共変・反変)の概念です。

「高階関数(関数を受け取ったり返したりする関数)を作りたいけれど、型定義でエラーが出てしまう……」そんな壁にぶつかったことはありませんか?
大丈夫。ここをクリアすれば、君はもうHackの型チェッカーを手なずけるプロフェッショナルです。一緒に本質を紐解いていきましょう!

—

1. そもそも「関数型」って何だっけ?

Hackでは、関数をただの処理の塊ではなく、「第一級オブジェクト(値)」として扱えます。つまり、変数に代入したり、他の関数の引数として渡すことができるわけです。

例えば、「整数を受け取って文字列を返す関数」を型として表現する場合、Hackでは次のように書きますよね。

// 整数を受け取り、文字列を返す関数型
(function(int): string)

この「関数を受け取る高階関数」を設計する時、「受け取る関数に、どれくらい柔軟な(あるいは厳格な)関数を渡せるか」という問題が出てきます。ここで登場するのが、反変(Contravariance)と共変(Covariance)という、一見難しそうに見えて実はとても理にかなったルールです。

—

2. 脳内イメージ図解:引数と戻り値の「世界線」

変性を理解するための最も簡単なコツは、「引数は逆向き(反変)、戻り値は同じ向き(共変)」という鉄則を覚えることです。

言葉だけだと混乱するので、次のような「動物病院」のシチュエーションを思い浮かべてみてください。

  • Animal(動物):すべての動物の親クラス
  • Dog(犬):Animalの子クラス(具体的な動物)

ここで、「犬(Dog)を診察できるはずの医者」を期待している場所に、あなたはどんな医者を連れて行きますか?

引数のルール(反変:Contravariance)

  • × 「チワワ専用の医者」を連れて行くと、柴犬が来たときに困ってしまいます(型安全性の崩壊)。
  • ○ 「すべての動物(Animal)を診られる獣医」を連れていけば、犬はもちろん、猫が来ても対応できますよね。

つまり、期待されている引数の型(Dog)よりも、「より広い型(Animal)」を受け取れる関数の方が、安全に代入できるのです。これが反変です。

戻り値のルール(共変:Covariance)

  • ○ 「動物(Animal)を何かしら返す関数」を期待されている場所に、「必ず犬(Dog)を返す関数」を渡せば、受け取る側は大喜びです。犬は確実にAnimalですからね。

つまり、返ってくるデータの型は、期待されている型(Animal)の「サブタイプ(Dog)」であればあるほど安全です。これが共変です。

—

3. 実践!Hackコードで挙動を体感する

百聞は一見にしかず。実際のStrict Modeのコードで、この挙動を確認してみましょう。

// strict
<<__Strict>>
namespace Hack\Excellence;

class Animal {}
class Dog extends Animal {}

class VarianceDemo {
// 引数として「Dogを受け取り、Animalを返す関数」を受け取る高階関数
public static function processDog(
(function(Dog): Animal) $operation
): Animal {
$myDog = new Dog();
// 渡された関数を実行する
return $operation($myDog);
}
}

では、この `processDog` に対して、どのような関数を渡すことができるでしょうか?

<<__Strict>>
namespace Hack\Excellence;

function run_demo(): void {
// ① 期待通りの関数
$fn1 = (Dog $d): Animal ==> new Animal();
VarianceDemo::processDog($fn1); // OK

// ② 引数を「より広い型(Animal)」にした関数 = 【反変の魔法】
// Dogを期待されている場所に、Animal全般を扱える関数を渡しても安全!
$fn2 = (Animal $a): Animal ==> new Animal();
VarianceDemo::processDog($fn2); // OK!型チェッカーは怒りません。

// ③ 戻り値を「より狭い型(Dog)」にした関数 = 【共変の魔法】
// Animalを返すことが期待されている場所に、確実にDogを返す関数を渡す!
$fn3 = (Dog $d): Dog ==> new Dog();
VarianceDemo::processDog($fn3); // OK!これも完璧に安全です。
}

🧠 ここがポイント!

Hackの型チェッカーは、HHVMのJITコンパイルの最適化を最大限に活かすため、実行時エラーを未然に防ぐこのチェックを極めて厳格に行います。もし、ここに違反する関数を渡そうものなら、型チェッカーがビルドを即座にストップさせてくれます。

—

4. やりがち? 陥りやすい文法エラーと対策

初学者がよくやってしまうミスを見ておきましょう。

❌ やりがちなミス:引数の型を「より狭く」してしまう

// 期待されているのは (Dog): Animal なのに…
$badFn = (Dog $d): Animal ==> {
// 内部でさらに絞り込んだ具体的なサブタイプを要求してしまう等…
};

// もしこれが逆の立場(コールバックの定義側)で起きると、
// 「お前が渡すデータ、本当にその型で大丈夫なのか?」と型チェッカーに叱られます。

【解決策】
高階関数に渡すコールバックやクロージャを定義する際は、「引数はより抽象的(上位の型)に、戻はより具体的(下位の型)に」という原則を思い出してください。迷ったら、図解した動物病院の例を脳内でリプレイしましょう。

—

まとめ:Hackの型制約は、君の最強の味方

今回は、Hackの関数型における共変・反変制約について解説しました。

  • 引数(Contravariant / 反変):受け取る型は、より広い型へ代入可能。
  • 戻り値(Covariant / 共変):返す型は、より狭い型へ代入可能。

最初は少し難解に感じるかもしれませんが、このルールが頭に染み込むと、複雑な非同期処理やイベント駆動アーキテクチャ、ミドルウェアのパイプライン設計を行う際に、バグをゼロにした堅牢なコードが書けるようになります。

ここをクリアした君なら、もうHackの基本はバッチリマスターできていますよ!
さあ、自信を持って次のコードを書きに行きましょう。Happy Hacking!

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