実行時エラーにサヨナラ!型チェッカーにビジネスルールを守らせよう
Webアプリケーションを開発していると、こんな経験はありませんか?
- 「メール認証が終わっていないユーザーなのに、課金処理が走ってしまった……」
- 「バリデーション前の汚染された入力値を、誤ってデータベースに保存してしまった……」
これらは一般的に、`if ($user->is_verified)` のような実行時(Runtime)の条件分岐を書き忘れることで発生します。どれだけ単体テストを書いても、人間の手によるチェックには限界がありますよね。
そこで今回は、Hackの強力な型チェッカー(`hh_client`)を活用し、ビジネスルール違反を「コンパイル(型チェック)エラー」として未然に防ぐ高等テクニック「Phantom Types(幽霊型)」をご紹介します!
「高等テクニック」と聞くと難しそうに思えるかもしれませんが、仕組みはとてもシンプルでエレガントです。ここをクリアすれば、Hackの型システムを自在に操る第一歩を踏み出せますよ!
—
Phantom Types(幽霊型)とは? 〜実体のない型パラメータの魔法〜
幽霊型(Phantom Type)とは、「クラスのプロパティやメソッドの引数としては一切使われないけれど、型パラメータとして宣言されている型」のことです。
通常、ジェネリクス(Generics)は次のように中身のデータを保持するために使いますよね。
// 通常のジェネリクス:T型のデータを実際に保持する
final class Box
public function __construct(private T $value) {}
public function get(): T { return $this->value; }
}
一方、幽霊型は次のように宣言します。
// 幽霊型:Tは型定義に存在するが、プロパティとしては保持されない!
final class User
// $stateのようなプロパティは存在しない!
public function __construct(
private int $id,
private string $name,
private string $email,
) {}
}
「プロパティで使わないなら、`TState` は何のためにあるの?」と思いますよね。
実はこの `TState` は、型チェッカーに対して「このインスタンスがいまどの状態にあるか」を伝える専用のラベル(付箋)として機能するのです。
—
【図解】従来のフラグ管理 vs 幽霊型による状態遷移
従来の「ステータスフラグ」による管理と、「幽霊型」による管理の違いをイメージで比較してみましょう。
【従来のフラグ管理】
$user: User (is_verified = false)
│
▼ 開発者が if 文を書き忘れると……
payBill($user); // 💥 実行時に問題発生!(型チェッカーは通ってしまう)
【幽霊型(Phantom Types)による管理】
$unverifiedUser: User
│
▼ sendVerificationEmail() を通す
$verifiedUser: User
│
▼
payBill(User
// 💡 payBill($unverifiedUser) と書いた瞬間、
// hh_client が「型が合わない!」と怒ってコンパイルを通さない!
幽霊型を使うと、「未検証状態のユーザーを、検証済み専用の関数に渡すコード」は 1 行たりとも実行すらできなくなります。型チェッカーが最強の防壁になってくれるわけですね。
—
実践:Hack Strict Mode で幽霊型を実装してみよう
それでは、実際のHackコードで幽霊型を構築してみましょう。Hackの厳格な型安全性を活かすため、ファイル冒頭にはしっかり型を意識した設計を行います。
1. 状態を表す「マーカー型」を定義する
まずは状態を表す型を用意します。これらはインスタンス化する必要がないため、中身は空の `interface` や `abstract final class` で構いません。
namespace MyProject\User;
// 状態を表すマーカー型(中身は空でOK)
interface UnverifiedState {}
interface VerifiedState {}
2. 幽霊型を持つ `User` クラスを実装する
ここが最大のポイントです。コンストラクタを `private` にして、外部から自由な状態の `User` を勝手に作れないようにカプセル化します。
namespace MyProject\User;
final class User
// 外部からの直接 new を禁止し、状態の偽装を防ぐ
private function __construct(
private int $id,
private string $name,
private string $email,
) {}
// 新規作成時は必ず「未検証(UnverifiedState)」として生成
public static function register(int $id, string $name, string $email): User
return new User
}
// 検証処理を行い、「検証済み(VerifiedState)」の User に状態遷移させる
public static function verify(User
// ここで実際のトークン検証ロジックを実行…
// 成功したら VerifiedState の型を持つ新しいインスタンスを返す
return new User
}
public function getId(): int { return $this->id; }
public function getName(): string { return $this->name; }
public function getEmail(): string { return $this->email; }
}
3. ビジネスロジックを型で制約する
課金処理など、「検証済みユーザー」にしか許可したくない処理の引数に `User
namespace MyProject\Billing;
use MyProject\User\User;
use MyProject\User\VerifiedState;
use MyProject\User\UnverifiedState;
// 検証済みユーザーのみを受け取る関数
function chargeMonthlyFee(User
// ここに到達した時点で、ユーザーが検証済みであることは「100%保証」されている!
// だから if ($user->isVerified()) などの防御的コードは一切不要!
echo “Charged {$amount} JPY to {$user->getName()}.\n”;
}
4. 実際に動かして型チェッカーの挙動を確認!
<<__EntryPoint>>
function main(): void {
// 1. ユーザーを新規登録(この時点では User
$user = User::register(1, ‘Alice’, ‘alice@example.com’);
// 💥 ここで課金しようとすると……?
// chargeMonthlyFee($user, 1000);
// ↑ コメントを外すと、hh_client が以下のエラーを即座に叩き出します!
// Invalid argument: Expected User
// 2. 正しく認証ステップを踏む
$verifiedUser = User::verify($user, ‘secret-token-123’);
// 3. 検証済みユーザーなら何の問題もなくパスする!
chargeMonthlyFee($verifiedUser, 1000);
}
—
なぜこの設計が美しいのか? 〜HHVMと型チェッカーの共演〜
このアプローチの素晴らしいところは、「型安全性を極限まで高めつつ、実行時のオーバーヘッドが実質ゼロ(ゼロコスト抽象化)」という点です。
HHVM(HipHop Virtual Machine)の内部実行において、ジェネリクスの型パラメータ `TState` 自体は実行時のメモリを消費しません。型チェッカー(`hh_client`)が静的解析の段階で状態の整合性をすべて検証し終えているため、本番環境のHHVMは余分な `is_verified` チェックフラグの評価をスキップでき、高速かつ安全に動作します。
—
初学者がハマりやすい落とし穴と解決策
幽霊型を使う際に、初心者のエンジニアが引っかかりやすいポイントを2つ紹介します。
落とし穴 1:コンストラクタを `public` にしてしまう
コンストラクタを `public` のままにすると、誰かが `new User
状態遷移を伴う幽霊型では、必ずコンストラクタを `private` にし、正規のファクトリメソッド経由でのみインスタンス化させるようにしましょう。
落とし穴 2:状態遷移時に中身を破壊的に書き換えようとする
Hackの設計思想として、状態遷移は「既存のインスタンスを書き換える(ミュータブル)」のではなく、「新しい状態のインスタンスを作って返す(イミュータブル)」スタイルが最も安全で、型チェッカーとも相性が抜群です。
—
まとめ:型を「制約の道具」として使いこなそう!
今回は、Hackの静的型システムをフル活用した Phantom Types(幽霊型) の基礎と実践テクニックをご紹介しました。
- Phantom Types は、実行時プロパティとしては使われない「状態ラベル用」の型パラメータ
- 型チェッカーの力 を借りて、不正な状態での処理呼び出しをコンパイル時に100%遮断できる
- 防御的な `if` 文が減り、コードの可読性とパフォーマンスが向上する
「実行時エラーが起きないように祈る」のではなく、「そもそも不正なコードは型チェックが通らないように設計する」。このマインドセットが身につくと、Hackでの開発が何倍も楽しく、そして強固なものになりますよ。
ぜひあなたのプロジェクトでも、状態遷移が複雑なドメインモデルに幽霊型を取り入れてみてくださいね!