Upgrade to Pro — share decks privately, control downloads, hide ads and more …

代数的データ型って何が嬉しいの? #frontend_phpcon_do

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

代数的データ型って何が嬉しいの? #frontend_phpcon_do

Avatar for Takuma Kajikawa

Takuma Kajikawa

June 05, 2026

More Decks by Takuma Kajikawa

Other Decks in Programming

Transcript

  1. 梶川 琢馬 𝕏 @kajitack 株式会社 TechBowl VPoT TechTrain の開発 /

    メンター PHP カンファレンスは多数参加 初フロントエンドカンファレンス 初北海道 X でスライド公開してます! x.com/kajitack 2/41
  2. 型に対する考え方 最近はフロントエンドを TS で書く。PHP も型が強化された。 向き合い方が変わった。 「ビット列の解釈」としてだけでなく、「集合」として捉える ビット列の解釈 文字列や数字、配列などデータの扱い方やデータの サイズを意識した型定義

    集合 bool → { true, false } string → { "", "a", ... } 「取りうる値はこれだけ」と値の範囲を決め、 どういう入力・処理なのかを実行せずに判断する ための型定義 TS 公式でも Types as Sets と紹介 https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-oop.html#types-as-sets 6/41
  3. 型を書くことは、集合を絞ること string のまま "pending" "paid" "shipped" "hoge" "" "payed" "PAID

    " "発送済み?" ありえない値の組み合わせが膨⼤ ドメインの知識で 絞る OrderStatus 型 "pending" "paid" "shipped" ありえない値は最初から⼊らない 7/41
  4. 代数的データ型 (ADT:Algebraic Data Types) 掛け算や足し算のように型を組み立てる 積(AND) 直積型(Class / Struct) 和(OR)

    列挙型(Enum / Union) 直和型(Discriminated Union / Tagged Union) データの仕様は突き詰めると「かつ」と「または」でできている 特に和(OR)が型を組み合わせる時に強力な武器になる 10/41
  5. フロントエンド・PHP カンファレンスで積と和 積(AND)で表すと... 2 × 2 = 4 通り。 isFE

    = false; isPHP = false は 防ぎたい 和(OR)なら... 1 + 1 + 1 = 3 通り。「どちらでもない」は そもそもありえない type Topic = {isFE: boolean; isPHP: boolean;} TypeScript type Topic = "FE" | "PHP" | "両方"; TypeScript 11/41
  6. 直積型(Product Type) 複数の型の値を、同時に保持する 代表例: オブジェクト型・class・struct・タプル 値の数は「名前の数 × 年齢の数」。掛け算で増えるから「直積」 // 直積:

    名前 かつ 年齢、両方を持つ type User = { name: string; age: number; }; TypeScript class User { public function __construct(public string $name, public int $age) {} } PHP 12/41
  7. 列挙型(Enumerated Type) 複数の値の選択肢のうち、どれか 1 つになる。ただし、構造(直積)を持てない。 代表例: enum・union (言語によっては直和型の場合もある) 値の数は 1

    + 1 + 1 = 3 通り。足し算で増える。 直和型の土台のような型 // 列挙: 赤・黄・青の、どれか1つ type Color = "red" | "yellow" | "blue"; TypeScript enum Color { case Red; case Yellow; case Blue; } PHP 13/41
  8. 直和型(Sum Type) 直積を持てる列挙型。一番重要。 代表例: 判別可能ユニオン(Discriminated Union)、Sealed クラス kind のような共通フィールドが判別子(タグ)。その値でどのケースかを見分け、型が 絞り込まれる

    値の数は「成功の数 + 失敗の数」。和の各ケースの中身が直積 // 直和: 成功(金額つき) または 失敗(エラーつき) type PaymentResult = | { kind: "success"; amount: number } | { kind: "failure"; error: string }; TypeScript 14/41
  9. 何が嬉しいのか? Make Illegal States Unrepresentable 仕様上あり得ない状態は表現しない。そしてコンパイラや静的解析がそれを理解する 特に直和型はケースごとに違う構造を持てるので強力 // 直積: 2

    × 2 = 4 通り。「両方 null」「両方あり」が作れてしまう type Session = { loginUser: User | null; // ログイン済みなら入る guestToken: string | null; // ゲストなら入る }; TypeScript // 直和: 1 + 1 = 2 通り。仕様通りの状態しか存在しない type Session = | { kind: "loggedIn"; user: User } | { kind: "guest"; token: string }; TypeScript https://functional-architecture.org/make_illegal_states_unrepresentable/ 16/41
  10. boolean や nullを積の組み合わせで管理する場合 「読み込み中なのにエラーもデータもある」が作れてしまう const [isLoading, setLoading] = useState(false); const

    [error, setError] = useState<Error | null>(null); const [data, setData] = useState<Data | null>(null); TypeScript // バラバラの useState も、まとめて見ればこの直積型 type FetchState = { isLoading: boolean; error: Error | null; data: Data | null; }; TypeScript 20/41
  11. 2 × 2 × 2 = 8 通り。だが有効なのは 4 つだけ

    isLoading error data 意味 false なし なし 未取得 true なし なし 読み込み中 false あり なし エラー false なし あり 取得済み true あり なし 読み込み中なのにエラー? true なし あり 読み込み中なのにデータ? false あり あり エラーなのにデータ? true あり あり 全部同時? 21/41
  12. That boolean should probably be something else boolean はたいてい「別の型」の成れの果て 一度きりの出来事

    — 「いつ起きたか」の情報を捨てない isConfirmed: boolean → confirmedAt: Date | null 種別・ステータス — 状態が増えても網羅チェックが効く isAdmin: boolean → role: "admin" | "editor" | "viewer" 判定結果 — true の意味が型から読めない(boolean blindness) check(user): boolean → Allowed | NotPermitted(reason) 出典: That boolean should probably be something else https://ntietz.com/blog/that-boolean-should-probably-be-something-else/ 22/41
  13. UI の状態をDiscriminated Unionで表す useState 3 本の直積をやめ、直和 1 本で持つ。取りうる 4 状態だけを、ぴったり表せる

    type AsyncData = | { kind: "notAsked" } | { kind: "loading" } | { kind: "success"; data: Data } | { kind: "error"; error: Error }; const [state, setState] = useState<AsyncData>({ kind: "notAsked" }); TypeScript 23/41
  14. 網羅的な型チェックができる never を使った網羅チェック(Exhaustiveness check)パターン switch (state.kind) { case "notAsked": case

    "loading": return null; case "error": return <ErrorMessage error={state.error} />; case "success": return <UserCard user={state.data} />; default: const _exhaustiveCheck: never = state; // 抜けがあれば型エラー return _exhaustiveCheck; } TSX https://typescriptbook.jp/reference/statements/never#neverを使った網羅性チェック 24/41
  15. 構造が違うデータの集まり メンターレコメンド チャット 学びたい技術を教えてください! type: "botText" React Go 設計 type:

    "tagSelect" type: "userText" React を学びたいです! おすすめのメンターはこちらです React Next.js React TS type: "mentorCards" 25/41
  16. 構造が異なるコンポーネントを判別可能ユニオンで表現 botText の処理中に mentors へアクセス → 型エラー。種類ごとの固有プロパティを 取り違えない レコメンドの計算待ちは {

    type: "mentorCards"; recommend: AsyncData } のように、直和の 中に直和を入れ子にして表せる type ChatMessage = | { type: "botText"; content: string } | { type: "userText"; content: string } | { type: "tagSelect"; tags: Tag[] } | { type: "mentorCards"; mentors: Mentor[] }; const messages: ChatMessage[] = [/* 会話の全メッセージ */]; TypeScript 26/41
  17. 描画も、状態と同じ網羅チェックが効く 次にどの種別が来るかは実行時に決まる。それでも、来うる種別は直和で閉じている 種別を増やしたら、直すべき描画コードがコンパイラが全部教えてくれる switch (message.type) { case "botText": return <BotBubble

    message={message} />; case "userText": return <UserBubble message={message} />; case "tagSelect": return <TagSelector message={message} />; case "mentorCards": return <MentorCardList message={message} />; default: const _exhaustiveCheck: never = message; // 種別の追加漏れは型エラー return _exhaustiveCheck; } TSX 27/41
  18. Result 型とは 成功か失敗か、どちらか一方を表す直和型 成功なら Ok が値 T を持ち、失敗なら Err がエラー

    E を持つ 失敗を「投げる」のではなく、値として「返す」 ただし、PHP にはない Result<T, E> = Ok(T) | Err(E) 32/41
  19. PHP で Result 型を実装する Sealed クラス (直和型) を、PHPStan(静的解析)で実現 /** *

    @phpstan-sealed Ok|Err (実装を特定のクラスだけに限定する) */ interface Result { public function isOk(): bool; public function unwrap(): mixed; } final readonly class Ok implements Result { /* 値 T を持つ */ } final readonly class Err implements Result { /* エラー E を持つ */ } PHP https://github.com/valbeat/php-result 33/41
  20. Result を使うと、エラー分岐も網羅できる 失敗が「ビジネスロジックの値」になる。 $result = $this->orderService->placeOrder($input); if ($result->isErr()) { return

    match ($result->unwrapErr()) { OrderError::UserNotFound => /* ... */, OrderError::OutOfStock => /* ... */, }; // ケース漏れは PHPStan が検出 } $order = $result->unwrap(); PHP 34/41
  21. PHP編のまとめ 想定できる失敗は「例外」ではなく、Result で値として返す データ付きの直和は Sealed クラスで作り、静的解析で閉じて絞り込む エラー分岐は match で網羅。ケース漏れは静的解析が検出する ADT

    の RFC が進行中。PHP の型アップデートに注目 今回紹介した Result 型以外のモデリングにも活かせるかも (今は Factory パターンで Immutable な Entity 生成をやっている) 37/41