Share
Embed
止まらない比較を止める — 余帰納法で無限ストリームを比べる
Scala の LazyList で作った2つの無限ストリームは、要素ごとに比べると、いつまでも比較が終わりません。この LT では、ストリームを生成する有限状態機械に注目し、余帰納法 (coinduction) と bisimulation の考え方を使って、無限の振る舞いの一致を判定します。最後に、この考え方をもとに、必ず停止する同値性判定を Scala で書きます。
INA Lintaro
November 13, 2026
Transcript
24 slides
-
比較が止まるのは、リストに終わりがあるから 先頭を比較して、残りを再帰的に比較する 最後は必ず Nil に到達するので、比較は止まる = = = 1 →
2 → 3 → Nil = def equal[A](xs: List[A], ys: List[A]): Boolean = (xs, ys) match case (Nil, Nil) => true case (x :: xt, y :: yt) => x == y && equal(xt, yt) case _ => false 1 → 2 → 3 → Nil -
比較が止まるのは、リストに終わりがあるから 先頭を比較して、残りを再帰的に比較する 最後は必ず Nil に到達するので、比較は止まる = = = 1 →
2 → 3 → Nil = def equal[A](xs: List[A], ys: List[A]): Boolean = (xs, ys) match case (Nil, Nil) => true case (x :: xt, y :: yt) => x == y && equal(xt, yt) case _ => false 1 → 2 → 3 → Nil -
Scala なら、終わりのないストリームも簡単に作れる 有限個の状態から、 LazyList で無限ストリームを生成する Machine 1 a (1) Machine
2 b (2) x (1) y (2) z (1) 括弧内は、その状態で観測される出力値 1 → 2 → 1 → 2 → ⋯ enum S1: case a, b def step1(s: S1): (Int, S1) = s match case S1.a => (1, S1.b) case S1.b => (2, S1.a) val stream1 = LazyList.unfold(S1.a)(s => Some(step1(s))) val stream2 = LazyList.unfold(S2.x)(s => Some(step2(s))) 一段ずつ構造を生成する = 余再帰 (corecursion) 1 → 2 → 1 → 2 → ⋯ enum S2: case x, y, z def step2(s: S2): (Int, S2) = s match case S2.x => (1, S2.y) case S2.y => (2, S2.z) case S2.z => (1, S2.y) -
同じ比較を無限ストリームにすると、止まらない def equal[A](xs: LazyList[A], ys: LazyList[A]): Boolean = (xs, ys)
match case (LazyList(), LazyList()) => true case (x #:: xt, y #:: yt) => x == y && equal(xt, yt) case _ => false 有限リスト LazyList List[A] LazyList[A] Nil LazyList() :: #:: 1 = 1 ✓ 2 = 2 ✓ 1 = 1 ✓ 2 = 2 ✓ ⋯ 終わらない -
同じ比較を無限ストリームにすると、止まらない def equal[A](xs: LazyList[A], ys: LazyList[A]): Boolean = (xs, ys)
match case (LazyList(), LazyList()) => true case (x #:: xt, y #:: yt) => x == y && equal(xt, yt) case _ => false 有限リスト LazyList List[A] LazyList[A] Nil LazyList() :: #:: 1 = 1 ✓ 2 = 2 ✓ 1 = 1 ✓ 2 = 2 ✓ ⋯ 終わらない -
同じ比較を無限ストリームにすると、止まらない def equal[A](xs: LazyList[A], ys: LazyList[A]): Boolean = (xs, ys)
match case (LazyList(), LazyList()) => true case (x #:: xt, y #:: yt) => x == y && equal(xt, yt) case _ => false 有限リスト LazyList List[A] LazyList[A] Nil LazyList() :: #:: 1 = 1 ✓ 2 = 2 ✓ 1 = 1 ✓ 2 = 2 ✓ ⋯ 終わらない でも、生成元の状態機械は有限なのだから、判定できてもよさそうでは? -
値でなく状態を見れば、止める手がかりがある Machine 1 a (1) ステップ Machine 2 b (2)
x (1) 状態対 出力 y (2) z (1) -
値でなく状態を見れば、止める手がかりがある Machine 1 a (1) Machine 2 b (2) x
(1) ステップ 状態対 出力 0 (a,x) 1=1 ✓ y (2) z (1) -
値でなく状態を見れば、止める手がかりがある Machine 1 a (1) Machine 2 b (2) x
(1) ステップ 状態対 出力 0 (a,x) 1=1 1 (b,y) ✓ 2=2✓ y (2) z (1) -
値でなく状態を見れば、止める手がかりがある Machine 1 a (1) Machine 2 b (2) x
(1) ステップ 状態対 出力 0 (a,x) 1=1 1 (b,y) 2 (a,z) ✓ 2=2✓ 1=1✓ y (2) z (1) -
値でなく状態を見れば、止める手がかりがある Machine 1 a (1) Machine 2 b (2) x
(1) ステップ 状態対 出力 0 (a,x) 1=1 1 (b,y) 2 (a,z) 3 (b,y) ← 再登場 ✓ 2=2✓ 1=1✓ 2=2✓ y (2) z (1) -
値でなく状態を見れば、止める手がかりがある Machine 1 a (1) Machine 2 b (2) x
(1) ステップ 状態対 出力 0 (a,x) 1=1 1 (b,y) 2 (a,z) 3 (b,y) ← 再登場 ✓ 2=2✓ 1=1✓ 2=2✓ (a,x) y (2) z (1) (b,y) (a,z) 状態対が、一度現れた組み合わせに戻った! どちらの機械も、次の状態は1つに決まる → 同じ状態対に戻れば、そこから先も同じ繰り返し R = {(a, x), (b, y), (a, z)} で全部 -
止めてよい根拠: 1ステップの確認で十分 状態対 R (a,x) 1=1✓ (b,y) 2=2✓ (a,z) 1=1✓
(a,x) (b,y) (a,z) どの状態対でも、次の2条件が成り立つ 1. 現在の出力値が一致する 2. 次の状態対も R に含まれる 出力値は一致? ✓1=1 ✓2=2 ✓1=1 次も R の中? ✓ (b,y) ✓ (a,z) ✓ (b,y) -
止めてよい根拠: 1ステップの確認で十分 (a,x) 1=1✓ (b,y) 2=2✓ どの状態対でも、次の2条件が成り立つ 1. 現在の出力値が一致する 2.
次の状態対も R に含まれる 出力値は一致? 次も R の中? (a,x) ✓1=1 ✓ (b,y) (b,y) ✓2=2 ✓ (a,z) (a,z) ✓1=1 ✓ (b,y) 2 より、何回遷移しても R の外に出ない 1 より、R の中では出力値が一致する だから、出力値はずっと一致する 状態対 R (a,z) 1=1✓ -
止めてよい根拠: 1ステップの確認で十分 (a,x) 1=1✓ (b,y) 2=2✓ 出力値は一致? 次も R の中?
(a,x) ✓1=1 ✓ (b,y) (b,y) ✓2=2 ✓ (a,z) (a,z) ✓1=1 ✓ (b,y) 2 より、何回遷移しても R の外に出ない 1 より、R の中では出力値が一致する だから、出力値はずっと一致する 状態対 R (a,z) 1=1✓ どの状態対でも、次の2条件が成り立つ 1. 現在の出力値が一致する 2. 次の状態対も R に含まれる このような関係を bisimulation (双模倣) という -
止めてよい根拠: 1ステップの確認で十分 (a,x) 1=1✓ (b,y) 2=2✓ 出力値は一致? 次も R の中?
(a,x) ✓1=1 ✓ (b,y) (b,y) ✓2=2 ✓ (a,z) (a,z) ✓1=1 ✓ (b,y) 2 より、何回遷移しても R の外に出ない 1 より、R の中では出力値が一致する だから、出力値はずっと一致する 状態対 R (a,z) 1=1✓ どの状態対でも、次の2条件が成り立つ 1. 現在の出力値が一致する 2. 次の状態対も R に含まれる このような関係を bisimulation (双模倣) という 「ずっと一致する」は、各組で1ステップ分を確かめれば示せる これが 余帰納法 (coinduction) -
余帰納法の考え方を、そのままコードにする def equivalent[S, T, A]( s: S, t: T, stepS:
S => (A, S), stepT: T => (A, T) ): Boolean = def loop(s: S, t: T, seen: Set[(S, T)]): Boolean = if seen.contains((s, t)) then true else val (vs, ns) = stepS(s) val (vt, nt) = stepT(t) vs == vt && loop(ns, nt, seen + ((s, t))) loop(s, t, Set.empty) 数学的な説明 条件1: 出力値が一致する 条件2: 次の状態対も確かめる 確かめた状態対を R に加える R に戻った = R が閉じた コード vs == vt loop(ns, nt, ...) seen + ((s, t)) seen.contains((s, t)) なぜ true と言える? (正しさ) seen は閉じた関係 = bisimulation → 余帰納法で「ずっと一致する」と言える 単なる最適化ではない なぜ止まる? (停止) 状態対は有限個なので、必ず 値が違って false か、 seen に戻って true -
余帰納法の考え方を、そのままコードにする def equivalent[S, T, A]( s: S, t: T, stepS:
S => (A, S), stepT: T => (A, T) ): Boolean = def loop(s: S, t: T, seen: Set[(S, T)]): Boolean = if seen.contains((s, t)) then true else val (vs, ns) = stepS(s) val (vt, nt) = stepT(t) vs == vt && loop(ns, nt, seen + ((s, t))) loop(s, t, Set.empty) 数学的な説明 条件1: 出力値が一致する 条件2: 次の状態対も確かめる 確かめた状態対を R に加える R に戻った = R が閉じた コード vs == vt loop(ns, nt, ...) seen + ((s, t)) seen.contains((s, t)) なぜ true と言える? (正しさ) seen は閉じた関係 = bisimulation → 余帰納法で「ずっと一致する」と言える 単なる最適化ではない なぜ止まる? (停止) 状態対は有限個なので、必ず 値が違って false か、 seen に戻って true -
余帰納法の考え方を、そのままコードにする def equivalent[S, T, A]( s: S, t: T, stepS:
S => (A, S), stepT: T => (A, T) ): Boolean = def loop(s: S, t: T, seen: Set[(S, T)]): Boolean = if seen.contains((s, t)) then true else val (vs, ns) = stepS(s) val (vt, nt) = stepT(t) vs == vt && loop(ns, nt, seen + ((s, t))) loop(s, t, Set.empty) 数学的な説明 条件1: 出力値が一致する 条件2: 次の状態対も確かめる 確かめた状態対を R に加える R に戻った = R が閉じた コード vs == vt loop(ns, nt, ...) seen + ((s, t)) seen.contains((s, t)) なぜ true と言える? (正しさ) seen は閉じた関係 = bisimulation → 余帰納法で「ずっと一致する」と言える 単なる最適化ではない なぜ止まる? (停止) 状態対は有限個なので、必ず 値が違って false か、 seen に戻って true -
余帰納法の考え方を、そのままコードにする def equivalent[S, T, A]( s: S, t: T, stepS:
S => (A, S), stepT: T => (A, T) ): Boolean = def loop(s: S, t: T, seen: Set[(S, T)]): Boolean = if seen.contains((s, t)) then true else val (vs, ns) = stepS(s) val (vt, nt) = stepT(t) vs == vt && loop(ns, nt, seen + ((s, t))) loop(s, t, Set.empty) 数学的な説明 条件1: 出力値が一致する 条件2: 次の状態対も確かめる 確かめた状態対を R に加える R に戻った = R が閉じた コード vs == vt loop(ns, nt, ...) seen + ((s, t)) seen.contains((s, t)) なぜ true と言える? (正しさ) seen は閉じた関係 = bisimulation → 余帰納法で「ずっと一致する」と言える 単なる最適化ではない なぜ止まる? (停止) 状態対は有限個なので、必ず 値が違って false か、 seen に戻って true -
余再帰で作ったものは、余帰納法で比べる 有限リスト → 再帰で最後まで比較できる 無限ストリーム → 最後まで比較することはできない 有限状態機械が生成する無限ストリーム → 状態対の関係が保たれることを確認すればよい
余再帰で作ったものを、余帰納法で比べる いつ使う?: 無限に続くものが一致することを示したいとき 状態が無限でも、閉じた関係が見つかれば一致を証明できる ただし、自動で判定できるとは限らない -
余再帰で作ったものは、余帰納法で比べる 有限リスト → 再帰で最後まで比較できる 無限ストリーム → 最後まで比較することはできない 有限状態機械が生成する無限ストリーム → 状態対の関係が保たれることを確認すればよい
余再帰で作ったものを、余帰納法で比べる いつ使う?: 無限に続くものが一致することを示したいとき 状態が無限でも、閉じた関係が見つかれば一致を証明できる ただし、自動で判定できるとは限らない その構造を生成する状態が有限なら、 余帰納法によって振る舞いの一致を判定できる