依存型

型が値に依存できる型システム。Π(依存関数型, 全称)と Σ(依存ペア型, 存在)を持つ。

プログラマ向けの一言

「長さ3の配列」「正の整数」「ソート済みリスト」を型として表現できる。 普通の型システムでは「number の配列」までしか言えないが、依存型では「長さが n の number の配列」と言える。

コードで理解する

// --- 普通の TypeScript ---
function head(arr: number[]): number {
  return arr[0]; // arr が空だったら? → undefined が返る → バグ
}

// --- 依存型があれば ---
// 「長さが1以上の配列」を型で表現
// function head(arr: Vec<number, n>): n > 0 ? number : never
// → 空配列を渡すとコンパイルエラー

// TypeScript のタプルで疑似的に:
function safeHead(arr: [number, ...number[]]): number {
  return arr[0]; // 必ず1つ以上ある → 安全
}
safeHead([1, 2, 3]); // OK
// safeHead([]);       // コンパイルエラー

依存型の核心: 型の中に値が入る

// 普通の型: 型と値は別の世界
type NumberArray = number[];  // 「number の配列」としか言えない

// 依存型: 型が値をパラメータに取れる
// Vec<number, 3>   ← 型の中に「3」という値が入っている
// Vec<number, n>   ← 型の中に変数 n が入っている

// Idris(依存型言語)での例:
// append : Vec n a -> Vec m a -> Vec (n + m) a
// ↑ 長さ n のベクタと長さ m のベクタを結合すると長さ n+m のベクタ
// → 型レベルで長さの計算をしている

// TypeScript で無理やり近いことをすると:
type Vec<T, N extends number> = N extends 0 ? [] : [T, ...Vec<T, Subtract<N, 1>>];
// ↑ 実際にはこの Subtract は標準で使えないので限界がある

何が嬉しいか

// 1. 行列の掛け算で次元ミスをコンパイル時に防ぐ
// multiply : Matrix<m, n> -> Matrix<n, p> -> Matrix<m, p>
// → n が一致しないとコンパイルエラー

// 2. printf の書式文字列と引数の一致をチェック
// printf "%d is %s" : Int -> String -> IO ()
// → 引数の型と個数を書式文字列から導出

// 3. ソート済みリストを型で表現
// insert : (x : Int) -> SortedList -> SortedList
// → 結果が本当にソート済みであることを型が保証

Π と Σ: カリー=ハワードの ∀ / ∃ 対応

依存型の中核は2つの型構築子。単純型の A -> B と A × B を「右側が左の値に依存できる」よう一般化したもの:

型読み単純型での退化形論理での対応
Π(x:A). B(x) 依存関数型「各 x:A に対し B(x) の値を返す関数」B が x に依存しなければ A -> B全称 ∀x. P(x)
Σ(x:A). B(x) 依存ペア型「ある x:A と、それに対する B(x) の値の組」B が x に依存しなければ A × B存在 ∃x. P(x)

これがカリー=ハワードの真価: 型 = 命題、プログラム = その証明。Π(n:Nat). Vec n a -> a(長さ n の非空ベクタから要素を取れる)という型を持つ関数を書けたこと自体が ∀n. ... の証明になる。Σ(n:Nat). Vec n a(「ある長さを持つベクタ」= 長さを隠した存在)は存在量化。Σ(p : SortedList). ... のように「証拠付きの値」を返すのにも使う。

-- Agda: ∀(全称)と∃(存在)が Π/Σ そのもの
filter : (A → Bool) → List A → Σ[ n ∈ ℕ ] Vec A n
--                                  ↑ 「ある長さ n が存在して、長さ n の Vec を返す」= ∃
replicate : (n : ℕ) → A → Vec A n
--          ↑ 「すべての n に対して長さ n の Vec を作れる」= ∀

依存型固有の難しさ

表現力の代償として、単純型・篩型にはない本質的な困難がある:

単純型 vs 篩型 vs 依存型

軸単純型篩型依存型
型に載るもの構造のみ基底型 + decidable 述語任意の値・計算
例Int -> Int{v:Int | v > 0}Π(n).Vec n a
検証構文的一致SMT が自動放電人手 / tactic で証明構築
型推論完全 (HM)述語は宣言、基底は推論ほぼ不可、宣言が基本
決定可能性決定可能述語を制限すれば決定可能一般に決定不能(全域性で回避)
表現力低中(範囲・整合性)高(任意の仕様・定理)
コスト最小注釈注釈 + 証明
代表Java, GoLiquid Haskell, F*Idris, Agda, Coq, Lean

→ 三段階の連続体。篩型は依存型から「型の中の計算」を削って自動化を得た中間点(篩型の比較表と対)。

トレードオフ

代表的な言語

押さえどころ(カード化候補)

関連

Graph