§2.3 도메인과 연속 함수
결과를 값이 아니라 정보로 비교한다
§2.2에서 같은 풀기 방정식을 만족하는 함수가 둘 이상 생겼다. 이 중 계산의 뜻으로 삼을 함수를 고르려면 후보를 비교할 기준이 필요하다.
SigmaBot V = Flat (State V)에는 평평한 순서(flat order)를 준다.
Flat은 종료 결과와 바닥을 갖는 전용 귀납 타입이다.
/-- 결과 집합에 바닥을 더한 평평한 타입. Reynolds §2.3의 이산 집합 리프팅이다. -/
inductive Flat (α : Type u) where
/-- 유한한 결과를 얻지 못한 바닥. -/
| none : Flat α
/-- 종료하여 얻은 결과. -/
| some : α → Flat α
deriving DecidableEq, BEq, Repr
일반 Option Nat에서는 some 1 ≤ some 2가 성립한다. Flat Nat에서는 서로 다른
두 종료 결과가 어느 방향으로도 비교되지 않는다. 관찰 함수 observe는 toOption으로
결과를 꺼내므로 반환 타입 Option Int를 유지한다.
none ⊑ none none ⊑ some σ some σ ⊑ some σ
서로 다른 두 종료 상태는 비교하지 않는다. some σ ⊑ some σ'이면 σ = σ'다.
이 순서는 “더 큰 수”나 “더 좋은 상태”를 말하지 않는다. none보다 some σ가 위인
이유는 계산 결과에 관한 정보가 하나 늘었기 때문이다.
상태 변환 함수의 순서는 이 순서를 입력마다 적용한다.
f ⊑ g ↔ ∀ σ, f σ ⊑ g σ
따라서 g가 f보다 위라는 말은, f가 이미 내던 종료 결과를 바꾸지 않으면서
f가 none이던 입력 중 일부에서 결과를 더 낼 수 있다는 뜻이다.
사슬은 유한 근사를 차례로 놓은 것이다
계산을 0단계, 1단계, 2단계로 점점 더 오래 허용하면 정보가 줄지 않는 열을 얻는다. 이런 증가 열을 사슬(chain)이라 부른다.
/--
사슬(chain) — 증가하는 가산 열.
`Monotone` 은 Mathlib 의 것이다. `∀ m n, m ≤ n → seq m ≤ seq n` 이고,
Reynolds 의 `x₀ ⊑ x₁ ⊑ ⋯` 를 모든 지수 쌍에 대해 적은 형태다. 이웃한 항에 대한
부등식만으로도 유도할 수 있지만, 이후 증명에서는 이 전이 폐쇄된 형태가 바로 쓰인다.
-/
structure Chain (α : Type u) [Preorder α] where
/-- 열 자체. -/
seq : ℕ → α
/-- 증가한다. -/
mono : Monotone seq
seq n은 n번째 근사이고 mono는 나중 근사가 앞선 근사의 정보를 잃지 않는다는
증거다. 이 정의가 요구하는 것은 가산 열 하나다. 임의의 유향 집합을 쓰는 더 일반적인
도메인 이론과는 범위가 다르다.
사슬 전체를 하나의 값으로 합칠 때는 최소 상한(least upper bound)을 쓴다.
- 상계(upper bound)
사슬의 모든 항보다 위에 있는 값이다.
- 최소 상한
모든 상계보다 아래에 있는 상계다. 사슬이 쌓아 온 정보만 합치고, 갑자기 새 정보를 보태지 않은 값이다.
프리도메인은 모든 사슬의 극한을 제공한다
Predomain은 각 사슬에 최소 상한을 골라 주고, 그 선택이 실제 최소 상한임을 증명으로
묶는다.
/--
프리도메인 — 모든 사슬이 최소 상계를 갖는 부분 순서 집합.
**`PartialOrder` 를 확장하지 않고 인스턴스 인자로 받는다.** 확장하면 Mathlib 이 이미
순서를 주는 타입에서 순서 경로가 둘이 되어 다이아몬드가 생긴다. 인자로 받으면 순서는
언제나 원래 것 하나다.
-/
class Predomain (α : Type u) [PartialOrder α] where
/-- 사슬의 최소 상계. Reynolds 의 `⨆ᵢ xᵢ`. -/
lub : Chain α → α
/-- 그 값이 실제로 최소 상계다. -/
lub_isLUB (c : Chain α) : IsLUB (Set.range c.seq) (lub c)
이 저장소의 Domain α는 프리도메인에 최소원 ⊥이 추가된 경우다. Lean에서는
Predomain α와 OrderBot α를 함께 요구하는 축약 이름으로 둔다. 최소 고정점의 유한
근사를 시작하려면 아무 종료 정보도 없는 ⊥이 필요하다.
Mathlib에는 이미 더 일반적인 순서론 구조가 있지만, 이 절에서는 Reynolds의 정의와 증명을
따라가기 위해 작은 Chain과 Predomain을 직접 만든다. 라이브러리 대응물을 가져와
정리를 한 줄로 끝내면 §2.3의 학습 대상이 사라진다.
연속성은 극한에서 정보가 갑자기 생기지 않게 한다
단조 함수는 두 근사 사이의 순서를 보존한다. 연속 함수는 여기서 더 나아가, 사슬을 끝까지 합친 뒤 함수를 적용한 값이 각 근사에 함수를 적용해 합친 값과 맞도록 한다.
/--
연속(continuous) — 사슬의 극한을 함수상의 극한으로 보낸다.
Reynolds도 이 보존 조건으로 연속성을 정의하고, `x, y, y, …` 사슬에서 단조성을
유도한다(§2.3, p. 31). 책은 정의역과 공역을 모두 프리도메인으로 둔다. 여기서는
공역에 부분 순서만 요구하고, 각 상 집합의 최소 상계가 `f c.lub`라고 직접 적는다.
공역도 프리도메인이면 책의 정의와 같다.
-/
def Continuous [PartialOrder α] [PartialOrder β] [Predomain α] (f : α → β) : Prop :=
∀ c : Chain α, IsLUB (f '' Set.range c.seq) (f c.lub)
이 정의는 등식 하나를 직접 쓰지 않는다. f c.lub가 f를 사슬의 각 항에 적용해 얻은
값들의 최소 상한이라고 말한다. 공역 전체에 Predomain 인스턴스를 요구하지 않고도
해당 상의 극한 보존을 표현하기 위해서다.
현재 정의에서는 연속성으로부터 단조성을 증명한다. x ⊑ y일 때
x, y, y, …라는 사슬을 만들면, 그 극한은 y다. 연속성이 이 사슬의 상에 대해
f x ⊑ f y를 준다.
명제 2.1은 연속성 증명에서 확인할 방향을 줄여 준다.
/--
**명제 2.1** — 단조 함수가 연속일 필요충분조건.
`f (⨆ᵢ xᵢ) ⊑ ⨆ᵢ f(xᵢ)` 한 방향만 확인하면 된다. 나머지는 단조성에서 나온다.
이 진술은 연속성 목표를 이미 성립하는 단조 방향과 별도로 증명해야 하는 한 방향으로
분해한다. §2.4에서 함수들의 연속성을 합성할 때 이 형태를 사용한다.
**책과의 차이**: Reynolds의 진술은 이 부등식을 흥미로운 사슬에만 요구한다. 여기서는
`Interesting` 술어를 추가하지 않고 모든 `Chain`에 요구하는 동치 형태를 쓴다.
-/
@[exercise "Prop 2.1" 3]
theorem continuous_iff_le [PartialOrder α] [PartialOrder β] [Predomain α] [Predomain β]
{f : α → β} (hf : Monotone f) :
Continuous f ↔ ∀ c : Chain α, f c.lub ≤ (c.map hf).lub단조성만으로 충분하지 않은 이유는 무한한 극한에서만 새 정보를 내는 함수가 있기 때문이다.
단조지만 연속이 아닌 함수
자연수의 유한 시작 구간을 하나씩 늘리는 사슬을 생각한다.
∅ ⊆ {0} ⊆ {0, 1} ⊆ {0, 1, 2} ⊆ ⋯
이 사슬의 극한은 자연수 전체다. f s = (s = ℕ)로 두면 각 유한 근사에서 f s는
거짓이고, 극한에서만 참이다.
/--
단조인데 연속이 아닌 함수가 있다.
`f s = (s = ℕ)` 이 그런 함수다. 단조인 이유는 `s = ℕ` 이고 `s ⊆ t` 면 `t = ℕ` 이기 때문이다.
연속이 아닌 이유는 이렇다. 시작 구간의 사슬은 극한이 `ℕ` 이므로 `f` 를 먹이면 참이 된다.
그런데 사슬의 **각 항**은 유한한 시작 구간이라 `f` 를 먹이면 전부 거짓이다.
상이 `{거짓}` 뿐인데 그 최소 상계가 참일 수는 없다.
이 반례는 단조성이 유한한 비교마다 정보를 보존해도 가산 근사의 극한에서 새 정보를
갑자기 만들 수 있음을 보인다. §2.4의 Kleene 사슬 계산에서 `f (⨆ xᵢ)`를
`⨆ f(xᵢ)`로 옮기려면 이 점프를 막는 연속성이 필요하다.
-/
@[exercise "§2.3 not-continuous" 2]
theorem exists_monotone_not_continuous :
∃ f : Set ℕ → Prop, Monotone f ∧ ¬ Continuous f
Reynolds는 수직 자연수 ℕ⊤을 사용한다. 이 저장소는 Mathlib의 완비 격자 인스턴스를
활용할 수 있는 Set ℕ으로 같은 구조의 반례를 옮겼다. 유한 근사에서는 보이지 않던 정보가
극한에서만 생긴다는 논점은 같다.
Flat 사슬의 극한
평평한 Flat α 사슬은 계속 none으로 남거나, 어느 시점에 some a가 나온 뒤
그 값으로 고정된다. 서로 다른 두 some 값은 비교되지 않으므로, 증가 사슬 안에서 종료
결과가 다른 값으로 바뀔 수 없다.
flatPredomain은 이 관찰을 프리도메인 인스턴스로 만든다. “언젠가 some a가 나오는가”는
일반적으로 판정할 수 없어서 극한 선택에 Classical.choice를 쓴다. 이 정의가
noncomputable인 것은 실행기의 결함이 아니다. 임의의 계산이 언젠가 끝나는지를 극한
함수가 계산해 준다면 정지 문제를 풀 수 있기 때문이다.
Monotone.continuous_of_lub_mem 정리는 평평한 정의역에서 단조 함수가 연속임을 보인다.
평평한 사슬의 극한은 새 값이 아니라 사슬이 실제로 지나간 값이므로, 극한에서 갑자기 생길
정보가 없다.
바닥을 더한 함수는 어떻게 합성하는가
값을 바꾸는 함수 f : α → β를 계산 결과에 적용하려면 Flat.map f를 쓴다.
바닥 입력은 바닥으로, Flat.some a는 Flat.some (f a)로 간다.
반면 g : α → D의 공역이 이미 바닥을 갖고 있다면 결과에 바닥을 또 더할 필요가 없다.
/-- §2.3(p. 34)의 source-lifting. 입력에 바닥을 더하고 그 바닥을 공역의 바닥으로 보낸다. -/
def sourceLift [Bot β] (f : α → β) : Flat α → β
| .none => ⊥
| .some a => f a
g : State V → Flat (State V)로 놓으면 sourceLift g가 앞서 쓴 liftBot g다.
이 둘은 함수 타입부터 다르다. Flat.map g의 결과는 Flat (Flat (State V))지만,
sourceLift g의 결과는 Flat (State V)다.
Reynolds 명제 2.4(p. 35)의 다섯 항을 Domain/LiftingLaws.lean에서 직접 증명한다.
-
(a) 바닥에서 바닥, 값에서
Flat.some (f a)를 내는 확장은Flat.map f하나다. -
(b) 바닥에서 바닥, 값에서
g a를 내는 확장은sourceLift g하나다. -
(c)
Flat.map (g ∘ f) = Flat.map g ∘ Flat.map f -
(d)
sourceLift (g ∘ f) = sourceLift g ∘ Flat.map f -
(e)
h ⊥ = ⊥이면sourceLift (h ∘ g) = h ∘ sourceLift g
(e)의 가설은 값 입력에서는 필요하지 않다. 바닥 입력에서 왼쪽은 바닥이고 오른쪽은
h ⊥다. h를 언제나 Flat.some 1을 내는 함수로 잡으면 두 값이 달라진다.
파일의 nonstrict_counterexample은 이 반례를 확인한다.
책과의 차이: 책의 입력 P는 임의의 프리도메인이다. 이 실습은 이산 집합의
Flat 리프팅에 한정한다. 공역 D는 평평할 필요가 없으며, 순서와 바닥을 갖추면
sourceLift_continuous가 연속성을 보인다. 기존의 비이산 순서를 보존하는 일반
P⊥ 구성과 리프팅 모나드의 법칙은 별도 후속 실습이다.
상태 변환 함수의 극한은 입력마다 잰다
while의 후보 의미는 함수 State V → SigmaBot V다. 최소 고정점 정리를 이 함수들
사이에서 쓰려면 함수 공간 자체에도 사슬의 극한이 있어야 한다.
/--
함수 공간은 점별로 프리도메인이다. 사슬의 극한은 자리마다 따로 잰 극한이다.
```
(⨆ₙ fₙ) x = ⨆ₙ (fₙ x)
```
상계·최소 확인은 모두 "자리 `x`를 고정하고 `β` 쪽 성질을 쓴다"로 내려간다.
-/
noncomputable instance piPredomain [PartialOrder β] [Predomain β] : Predomain (α → β) where
lub c := fun x => (c.apply x).lub
lub_isLUB c := α:Type uβ:Type vinst✝¹:PartialOrder βinst✝:Predomain βc:Chain (α → β)⊢ IsLUB (Set.range c.seq) fun x => (c.apply x).lub
α:Type uβ:Type vinst✝¹:PartialOrder βinst✝:Predomain βc:Chain (α → β)⊢ (fun x => (c.apply x).lub) ∈ upperBounds (Set.range c.seq)α:Type uβ:Type vinst✝¹:PartialOrder βinst✝:Predomain βc:Chain (α → β)⊢ (fun x => (c.apply x).lub) ∈ lowerBounds (upperBounds (Set.range c.seq))
α:Type uβ:Type vinst✝¹:PartialOrder βinst✝:Predomain βc:Chain (α → β)⊢ (fun x => (c.apply x).lub) ∈ upperBounds (Set.range c.seq) α:Type uβ:Type vinst✝¹:PartialOrder βinst✝:Predomain βc:Chain (α → β)n:ℕx:α⊢ c.seq n x ≤ (fun x => (c.apply x).lub) x
All goals completed! 🐙
α:Type uβ:Type vinst✝¹:PartialOrder βinst✝:Predomain βc:Chain (α → β)⊢ (fun x => (c.apply x).lub) ∈ lowerBounds (upperBounds (Set.range c.seq)) α:Type uβ:Type vinst✝¹:PartialOrder βinst✝:Predomain βc:Chain (α → β)g:α → βhg:g ∈ upperBounds (Set.range c.seq)x:α⊢ (fun x => (c.apply x).lub) x ≤ g x
All goals completed! 🐙
(⨆ₙ fₙ) σ는 입력 σ 하나를 고정한 뒤 결과들의 평평한 사슬을 합친 값이다.
어떤 입력에서는 3단계에 종료 결과가 나타나고, 다른 입력에서는 100단계에 나타나도 된다.
함수 공간의 극한은 그 차이를 입력마다 보존한다.
명제 2.2는 연속 함수들만 모은 공간도 프리도메인임을, 명제 2.3은 항등·상수·합성으로
연속 함수를 조립할 수 있음을 보인다. 2장의 while에는 전체 함수 공간의 점별 극한이
직접 쓰이고, 연속 함수 공간은 이후 고차 함수의 타입 의미에서 다시 필요해진다.