こんにちは!Hack言語の世界へようこそ。チーフアーキテクトの私です。
他の言語(例えばPHPやTypeScript、Javaなど)からHackの世界に飛び込んできた開発者が、最初に感動し、そして同時に「おや?」と立ち止まる壁があります。それが今回取り上げる「Shape(シェイプ)」と「構造的部分型」です。
「strictモードでコードを書いていたら、配列のつもりで扱っていたデータ構造で型エラーが出てしまった…」
「インターフェースとどう使い分ければいいのか分からない…」
そんな悩みを持っていませんか?
大丈夫です。ここをクリアすれば、HHVMの型チェッカーを味方につけ、堅牢で爆速なコードを書くための基本はバッチリマスターできますよ。今日は、その本質を優しく、そして深く紐解いていきましょう。
—
1. Hackの「Shape」ってそもそも何?
一般的なプログラミング言語では、連想配列(キーと値のペア)を扱うとき、中身の構造がどうなっているかは実行時まで分からないか、あるいは面倒なクラス(データ転送オブジェクト:DTO)をいちいち定義する必要がありました。
Hackの Shape は、そのジレンマを鮮やかに解決する機能です。
Shapeを使うと、「この連想ペースには、こういうキー名があって、それぞれの値は必ずこの型でなければならない」という構造を、静的に(プログラムを実行する前に)ピタリと定義できます。
基本的な使い方とイメージ
まずはコードを見てみましょう。Strictモードの世界では、次のようにShapeを定義します。
<<__Strict>>
namespace HackMaster;
// ユーザー情報を表すShapeの型エイリアスを定義します
type UserShape = shape(
‘id’ => int,
‘name’ => string,
‘is_active’ => bool,
);
function print_user_info(UserShape $user): void {
// $user[‘name’] が string であることは型チェッカーが保証しています!
echo “ユーザー名: ” . $user[‘name’] . “\n”;
}
<<__EntryPoint>>
function main(): void {
// 正しい構造のデータを渡す
$valid_user = shape(
‘id’ => 1,
‘name’ => ‘Alice’,
‘is_active’ => true,
);
print_user_info($valid_user); // 正常に動作します
}
この `shape(…)` は、HHVMのエンジンレベルでは効率的な配列として扱われながらも、コンパイル時には厳格な型チェックの対象になります。パフォーマンスを犠牲にせず、安全性だけを手に入れるHackらしいアプローチですね。
—
2. 構造的型付け(Structural Subtyping)の魔力
さて、ここからが本題です。他の多くの言語(JavaやC#など)は「公称的型付け(Nominal Subtyping)」を採用しています。これは、「名前が一致しているか」「明確に `implements` しているか」で型の互換性を決める方式です。
しかし、HackのShapeは「構造的型付け」の世界に生きています。
これはどういうことか?図解的にイメージしてみましょう。
[ 求められているShape ]
- id (int)
- name (string)
[ 実際に渡されたShape ]
- id (int)
- name (string)
- email (string) <-- 余分なキーがある!
HackのStrictモードでは、「要求されたキーと型をすべて持っていれば、余分なキーが含まれていても部分型(Subtype)とみなして受け入れる」というルールがあります。これを専門用語で構造的部分型と呼びます。
実際のコードで挙動を確認してみましょう
<<__Strict>>
namespace HackMaster;
type BaseUser = shape(
‘id’ => int,
‘name’ => string,
);
type DetailedUser = shape(
‘id’ => int,
‘name’ => string,
‘email’ => string, // 詳細な情報が追加されている
);
function display_basic_info(BaseUser $user): void {
echo “ID: ” . $user[‘id’] . “, Name: ” . $user[‘name’] . “\n”;
}
<<__EntryPoint>>
function run_demo(): void {
$detailed: DetailedUser = shape(
‘id’ => 42,
‘name’ => ‘Bob’,
‘email’ => ‘bob@example.com’,
);
// おおっ! DetailedUser は BaseUser の構造を満たしているため、
// BaseUser を要求する関数にそのまま渡せます!
display_basic_info($detailed);
}
この柔軟性、すごくないですか?「APIから返ってきたリッチなデータの一部だけを処理する汎用関数」を作るときに、この構造的部分型が猛烈な威力を発揮します。
—
3. 陥りやすい文法エラーと注意点
非常に便利なShapeですが、厳格なStrictモードだからこそハマりやすいポイントがあります。現場でよくあるエラーを事前に予習しておきましょう。
罠1: オプショナルキーの扱い
デフォルトでは、Shapeのキーはすべて「必須」です。「あってもなくてもいいよ」というキーを定義したい場合は、`?` を使ってオプショナル(Optional)にする必要があります。
type UserWithNickname = shape(
‘name’ => string,
?’nickname’ => string, // ‘nickname’ は無くてもOK
);
もし `?` をつけずにキーを省略すると、型チェッカーに「キーが足りません」と怒られます。
罠2: 構造的型付けの「厳しさ」
構造的型付けは寛容なようでいて、実は非常に厳格です。
- キーのスペルミスは絶対に許されません(実行時エラーではなく、型チェッカーがビルドを即座に止めます)。
- 期待されている型と違う型(例えば `int` を期待しているところに `string`)が渡されると、もちろんコンパイルエラーになります。
—
4. インターフェース(Interface) vs Shape:どう使い分けるべきか?
「データ構造を表現するなら、クラスやインターフェースを使えばいいのでは?」という疑問が湧いてきますよね。ここがアーキテクトとしての腕の見せ所です。次のように使い分けるのが、Hackにおけるベストプラクティスです。
| 特徴 | Shape (`shape`) | インターフェース (`interface`) / クラス |
| :— | :— | :— |
| 主な用途 | データの入れ物(DTO)、APIレスポンス、JSONの構造定義 | ドメインロジック、振る舞い(メソッド)を持つオブジェクト |
| 定義の軽さ | 極めて軽量(型エイリアスを書くだけ) | ボイラープレート(class宣言、コンストラクタ等)が必要 |
| 型の性質 | 構造的型付け(形が合っていればOK) | 公称的型付け(明示的な実装が必要) |
| ミュータビリティ| 基本的にイミュータブルなデータ構造として扱う | メソッドを通じて内部状態を安全に変更・制御できる |
黄金の指針
- 「振る舞い(メソッド)を持たせず、純粋にデータのやり取りだけに使う(DTO)」のであれば、Shapeを使いましょう。
- 「ビジネスロジックを持たせたい」「状態の変更をメソッドでカプセル化したい」「ポリモーフィズム(多態性)を利用したい」のであれば、インターフェースやクラスを使いましょう。
—
まとめ
今回は、HackのStrictモードにおける「Shapeの構造的部分型」と「インターフェースの使い分け」について解説しました。
- HackのShapeは、単なる連想配列ではなく、静的型チェックの守護神がついた強力な構造化データ型である。
- 構造的部分型により、必要十分なキーを持っていれば拡張されたデータもスムーズに受け渡せる。
- データの保持にはShape、振る舞いにはインターフェースと、適材適所で使い分けることでコードの美しさが劇的に向上する。
この概念をモノにすれば、HHVMの型チェッカーはあなたを縛る鎖ではなく、最高の相棒に変わります。
さあ、自信を持って次のHackコードを書きに行きましょう!