【入門編】【中級者向け】構造的部分型と名前付き型の境界線:設計時に迷わないための使い分け基準 – Hack言語 コア・静的型システムとHHVMのアーキテクチャ解析バイブル

こんにちは!Hack言語の世界へようこそ。
日々、HHVMの高速なランタイムや厳格な型チェッカーに向き合っていると、「型とは何か?」という本質的な問いにぶつかることありませんか?

他の言語(例えばPHPやJavaなど)からHackを学び始めたばかりの頃は、「クラスを作って名前を付ければ間違いない」と思いがちですよね。でも、Hackの強みであるStrict Mode(厳格モード)を深く理解していくと、データの器として「クラス(名前付き型)」を使うべき場面と、「Shape(構造的部分型)」を使うべき場面の美しい境界線が見えてきます。

今回は、中級者の一歩先を目指すあなたに向けて、この「構造的部分型」と「名前付き型」の使い分けの極意を、優しく、そしてディープにお伝えしていきますね。ここをクリアすれば、Hackの設計スキルは一段と洗練されますよ!

—

1. そもそも「名前付き型」と「構造的部分型」って何が違うの?

まずは、頭の中を整理するために、この2つのアプローチの本質を確認しておきましょう。

  • 名前付き型(Nominal Typing):クラスやインターフェース
  • 「私は『User』という名前の証明書を持っています!」と、名前(アイデンティティ)で型が決まる仕組みです。
  • 構造が全く同じであっても、クラス名が違えば別の型として厳格に弾かれます。
  • 構造的部分型(Structural Subtyping):Shape型やTuple型
  • 「こういうキーと値の構造(形)をしていれば、それでOKです!」と、中身の構造(プロパティの形)だけで型が決まる仕組みです。
  • 名前ではなく、シェイプ(形)が一致しているか、あるいは要求される構造を満たしているかが評価されます。

図解的にイメージするなら、こんな感じです。

【名前付き型 (Nominal)】
[ Class: User ] <--- 「User」という看板が絶対必要! 【構造的部分型 (Structural / Shape)】 shape('id' => int, ‘name’ => string) <--- 看板は不要、中身の「形」が合っていればOK! Hackの `shape` は、まさにこの構造的部分型の代表格です。では、実際のコードを見ながら、それぞれの挙動と設計の迷いどころを見ていきましょう。 ---

2. 具体例で体感する:Shape型の手軽さと危うさ

まずは、APIのレスポンスや、一時的なデータの受け渡しで大活躍する `shape` の基本からおさらいです。

<>
namespace HackGuide;

// ユーザー情報の「形」をShapeで定義する
type TUserProfile = shape(
‘id’ => int,
‘name’ => string,
‘email’ => string,
);

function print_user_summary(TUserProfile $user): void
{
// $userはshapeなので、キーアクセスが安全に保証される
echo “User: ” . $user[‘name’] . ” (” . $user[‘email’] . “)\n”;
}

function run(): void
{
// 完全に同じ形を持つデータ
TUserProfile $user = shape(
‘id’ => 1,
‘name’ => ‘Alice’,
‘email’ => ‘alice@example.com’,
);

print_user_summary($user);
}

非常にシンプルで直感的ですよね。「クラスをわざわざ定義するほどでもない、だけど型安全にデータを扱いたい!」という時には、Shapeは最高の相棒になります。

ここでハマりがち!「余分なフィールド」に関する落とし穴

構造的部分型を扱う上で、初心者が一番混乱しやすいのが「定義されていない追加のキー」を渡した時の挙動です。Hackでは、デフォルトのshapeは「指定されたキーがすべて存在すること」を要求しますが、未定義のキーが含まれている場合の扱いは、そのshapeがどのように宣言されているかに依存します。

例えば、次のようなケースを考えてみましょう。

type TBaseUser = shape(‘name’ => string);

function greet(TBaseUser $u): void {
echo “Hello, ” . $u[‘name’] . “\n”;
}

// より多くの情報を持つshape
type TAdminUser = shape(‘name’ => string, ‘permissions’ => vec);

function run_trap(TAdminUser $admin): void {
// 質問:TAdminUserは、TBaseUserが要求する「形」を満たしているでしょうか?
// 実は、Strict Modeの型チェッカーはここで厳格に型エラーを出します!
greet($admin);
}

「あれ? `TAdminUser` には `name` が含まれているんだから、`TBaseUser` の代わりになれる(構造的部分型として受け入れられる)はずでは?」と思いますよね。
しかし、Hackの通常の `shape` は、「定義されたキー以外の余分なキーが含まれていてはならない(あるいは厳密に一致するべき)」という制約が働くため、そのままでは代入や引数の受け渡しで型エラーになることがあります(※最新のHackでは部分的な互換性のルールもありますが、基本のメンタルモデルとして「shapeの構造は厳密に一致することが求められる」と覚えておくのが安全です)。

—

3. 設計で迷ったときの「使い分け基準」

「じゃあ、いつクラス(名前付き型)を使って、いつShapeを使うべきなの?」
ここが今回の記事の一番重要な核心部分です。以下の3つの基準を物差しにしてみてください。

基準A:振る舞い(メソッド)を持つか?

  • クラスを使うべき
  • データだけでなく、そのデータを操作するビジネスロジック(メソッド)が紐づく場合は、迷わずクラス(またはRecordなど)を使いましょう。オブジェクト指向の基本原則ですね。
  • Shapeを使うべき
  • データ構造(DTO的なもの)だけであり、状態を変更するメソッドや複雑な振る舞いが一切ない場合。

基準B:ドメインモデルとしての「アイデンティティ」があるか?

  • クラスを使うべき
  • データベースのエンティティのように、「IDが同じなら同じユーザーである」といった固有のアイデンティティや、システム全体で一意に追跡されるべき概念には、名前付き型(クラス)が適しています。
  • Shapeを使うべき
  • データベースから取得した一時的なJOIN結果の配列、外部APIから受け取ったJSONのペイロードなど、「ただのデータの塊(Value Object的なもの、あるいはシリアライズ可能な構造)」である場合。

基準C:ライフサイクルと変更のスコープは?

  • クラスを使うべき
  • アプリケーション全体で共有され、あちこちのサービス層を旅するデータ構造。型を変更したときに、影響範囲をクラス名やインターフェースで綺麗にコントロールしたい場合。
  • Shapeを使うべき
  • 特定の関数やメソッドの内部、あるいは限られたモジュール間だけで完結する、いわゆる「ローカルなデータ構造」である場合。

—

4. まとめ:Hackの厳格さを味方につけた美しい設計へ

今回は、構造的部分型(Shape)と名前付き型(クラス)の境界線について、設計の視点から解説しました。

  • Shape型は、軽快で柔軟な「データの形」の定義に向いています。ただし、構造の厳密さやスコープの管理に少し気を使う必要があります。
  • 名前付き型(クラス)は、揺ぎないアイデンティティと、データ+振る舞いのカプセル化(オブジェクト指向的責務)を私たちに提供してくれます。

Hackの静的型チェッカーは、私たちが適当なコードを書くのを厳しく、しかし愛を持って見守ってくれる最高のパートナーです。「このデータはただの構造の集まりかな? それとも意味を持ったドメインの主役かな?」と一呼吸置いて自問してみてください。それだけで、あなたの書くHackコードは見違えるほど堅牢で美しいものになりますよ。

ここをクリアできれば、もうHackの型システムで迷うことはありません。
それでは、快適なHackライフを!次回の記事もお楽しみに!

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