반복문의 전체 뜻을 바로 재귀 정의하지 않고, 반복을 한 번 펼치는 연산자 F를 먼저 만든다.
w를 “남은 반복의 뜻”이라고 놓으면 한 바퀴는 다음처럼 쓸 수 있다.
/--
`while` 한 바퀴. `w`가 "반복의 나머지"다.
조건이 거짓이면 그 자리에서 끝나고, 참이면 본체를 한 번 돈 뒤 나머지 `w`에 넘긴다.
`s`는 본체의 뜻 — `Comm.eval`에서 `⟦c⟧`가 들어올 자리다.
-/defwhileF(b:BoolExpV)(s:StateV→SigmaBotV)(w:StateV→SigmaBotV):StateV→SigmaBotV:=funσ=>if⟦b⟧ᵇσthenFlat.bind(sσ)welseFlat.someσ
조건 b와 본체의 뜻 s를 고정하면 whileF b s는 함수에서 함수로 가는 연산자다.
F : (State V → SigmaBot V) → (State V → SigmaBot V)
전체 반복 의미 w가 풀기 방정식을 만족한다는 말은 F w = w, 곧 w가 F의
고정점이라는 말과 같다. §2.2에서 확인했듯 고정점은 여러 개일 수 있다.
왼쪽에서 첫 등식을 허용하는 가정이 연속성이다. 오른쪽 사슬은 원래 사슬에서 맨 앞의 ⊥만
뺀 사슬이고, ⊥은 모든 항 아래에 있으므로 극한이 바뀌지 않는다.
/--
**최소 고정점 정리, 둘째 단계 — 극한은 고정점이다.**
`F`의 연속성을 반복의 사슬에 적용하면 `F(fix)`가 "F를 입힌 사슬"의 극한이 된다.
그 사슬은 원래 사슬을 한 칸 민 것이고, 밀어도 극한은 그대로다. 극한의 유일성으로 끝난다.
**책과의 차이**: 단조성 `hm`을 연속성 `hF`에서 `hF.monotone`으로 뽑지 않고 따로 받는다.
`Continuous.monotone`은 완성된 정리로 제공되므로 `hF.monotone`을 `hm`에 넣을 수 있다.
여기서는 반복 사슬을 만드는 단조성과 그 극한을 보존하는 연속성의 역할을 드러내려고 분리한다.
-/@[exercise"§2.4 fix-eq"3]theoremfix_eq{F:α→α}(hm:MonotoneF)(hF:ContinuousF):F(fixFhm)=fixFhm
여기서 단조성만 가정하면 반복 사슬은 만들 수 있지만 F를 극한 안으로 옮길 수 없다.
§2.3의 단조지만 연속이 아닌 반례가 이 단계에서 연속성이 필요한 이유를 보여 준다.
F x ⊑ x인 값을 전고정점(pre-fixed point)이라 부른다. x가 고정점이면 당연히
전고정점이다. 반복 사슬의 모든 항이 x 아래에 있음을 자연수 귀납으로 보이면, 그 극한도
x 아래에 있다.
/--
**최소 고정점 정리, 셋째 단계 — 전고정점 아래에 있다.**
`F(x) ⊑ x`이면 `fix ⊑ x`다. 특히 `F(x) = x`이면 `F(x) ⊑ x`이므로, `fix`는 모든
고정점 아래에 있다.
§2.2의 두 해로 돌아가면 `decrFake`도 풀기 방정식의 해이므로 `fix ⊑ decrFake`다.
구체적인 감소 반복문의 뜻이 `decrTrue`와 같다는 계산은 책의 연습 2.3에 해당한다.
-/@[exercise"§2.4 fix-least"2]theoremfix_least{F:α→α}(hF:MonotoneF){x:α}(hx:Fx≤x):fixFhF≤x
최소라는 말은 “실행 시간이 가장 짧다”거나 “결과 상태의 정수가 가장 작다”는 뜻이 아니다.
정보 순서에서 아래라는 뜻이다. 최소 고정점은 유한 근사로 확인할 수 없는 입력에 임의의
종료 결과를 붙이지 않는다. §2.2의 decrFake가 탈락하는 이유가 여기에 있다.
/--
**Scott 귀납법.** 허용 가능한 성질이 `⊥`에서 성립하고 `F`가 보존하면, `fix F`에서
성립한다.
증명은 두 단계다. 반복의 각 단계에서 성질이 성립함을 자연수 귀납으로 확인하고,
허용 가능성이 그것을 극한으로 넘긴다. 어려움은 "허용 가능한가"를 확인하는 쪽에
있다는 것이 이 원리의 특징이고, §2.5부터 그 확인이 실제 일이 된다.
-/@[exercise"§2.4 scott"2]theoremscott_induction{F:α→α}(hF:MonotoneF){P:α→Prop}(hadm:∀c:Chainα,(∀n,P(c.seqn))→Pc.lub)(hbot:P⊥)(hstep:∀x,Px→P(Fx)):P(fixFhF)
일반적인 구조적 귀납법은 구문 트리의 생성자를 따라간다. Scott 귀납법은 의미의 유한
근사를 따라간다. while의 의미는 구문 재귀만으로 만들어지지 않으므로 두 귀납법의
역할도 다르다.
/--
`⟦c⟧ᶜ` — 명령의 뜻. Reynolds §2.2의 `⟦-⟧comm ∈ ⟨comm⟩ → Σ → Σ⊥`, §2.4에서 완성된 판.
여섯 절 가운데 `wh` 절에서만 최소 고정점이 필요하다. 반복의 뜻은 `whileF`의 최소
고정점이고, "최소"가 §2.2에서 확인한 여러 해 중 하나를 고르는 원리다.
`Classical.choice`를 거치므로 계산되지 않는다. 실행은 연료 해석기가 맡는다.
-/noncomputabledefComm.eval:CommV→StateV→SigmaBotV|.assignve=>funσ=>Flat.some(σ[v:=⟦e⟧ₑσ])|.skip=>funσ=>Flat.someσ|.seqc₀c₁=>funσ=>Flat.bind(c₀.evalσ)c₁.eval|.itebc₀c₁=>funσ=>if⟦b⟧ᵇσthenc₀.evalσelsec₁.evalσ|.whbc=>fix(whileFbc.eval)(whileF_monotonebc.eval)|.newvarvec=>funσ=>restorevσ(c.eval(σ[v:=⟦e⟧ₑσ]))
Comm.eval_isSemantics는 이 정의가 §2.2의 여섯 방정식을 모두 만족한다고 증명한다.
Comm.eval_while_least는 같은 본체와 조건에 대한 다른 풀기 방정식의 해보다 이 의미가
아래에 있음을 증명한다. 존재, 방정식 만족, 최소성이 서로 다른 정리로 나뉘어 있다.
Comm.eval은 noncomputable이다. 평평한 사슬에서 언젠가 종료 결과가 나타나는지를
일반적으로 계산할 수 없기 때문이다. 증명에 쓸 뜻과 실제로 돌릴 해석기를 분리해야 한다.
Comm.approxChain은 이 의미들을 사슬로 묶는다. Comm.eval_while_eq_approx_lub가
그 사슬의 극한과 while의 의미를 연결한다. 본체에 또 while이 있어도 이 대응은 같다.
다만 본체의 뜻은 완전한 c.eval이므로, 중첩 반복까지 연료를 제한하는 Comm.run n과는
단계별로 구별해야 한다.
책 pp. 37–38의 예제는 while x ≠ 0 do (x := x - 1; y := y + x)다.
처음 x = 3, y = 10이면 본체는 (2,12), (1,13), (0,13) 순서로 상태를 바꾼다.
그 뒤 조건이 거짓인지 확인하는 검사까지 필요하다.
w₀, w₁, w₂, w₃의 결과는 Flat.none이다.
w₄ 이후에는 x = 0, y = 13인 같은 종료 상태를 얻는다.
SumApproximation.lean의 sumApprox n은 이 본체의 계산을 직접 적은 함수다.
sumApprox_eq_approx_eval이 그 값과 실제 명령 wₙ의 의미를 연결한다.
연습 sumApprox_eq에서는 모든 정수 상태에 대해 다음 식을 증명한다.
Fⁿ(⊥)(σ) = if 0 ≤ σ(x) < n
then some (σ[x := 0][y := σ(y) + σ(x)·(σ(x)−1)/2])
else none
핵심은 n에 대한 귀납이다. x ≠ 0에서 한 번 감소한 입력의 조건
0 ≤ x−1 < n은 0 ≤ x < n+1과 같다. 결과 상태의 합산값도 같아야 하며,
그 부분은 triangle_step과 sumResult_step이 완성 자료로 제공한다.
학생은 상태를 바꾸어 귀납 가설을 적용하고, 종료 검사 횟수의 경계를 증명한다.
x ≥ 0이면 어느 유한 단계부터 결과가 같아진다. x < 0이면 모든 단계가 바닥이다.
sumLoop_eval_of_approx는 이 두 사슬의 최소 상계를 계산한다. 마지막 정리는 실제로
증명한 유한 근사식을 사용하므로, 추가 가설 없이 반복문 전체의 뜻을 준다.
/--
Reynolds p. 38. 합산 반복문의 최소 고정점은 `x ≥ 0`에서만 종료하는 이 전체 상태 함수다.
유한 근사식은 `sumApprox_eq`로 증명했으므로 추가 가설이 없다.
이 정리는 그 연습에 의존하는 결론이며 별도 채점 연습으로 두지 않는다.
-/theoremsumLoop_eval_closed(σ:StateString):sumLoop.evalσ=if0≤σ"x"then.some(sumResultσ)else.none:=sumLoop_eval_of_approxsumApprox_eqσ
직접 확인할 입력은 x = 0, x = 3, x = -1이다. 각각 처음 검사에서 종료,
세 번 본체 실행 뒤 종료, 영원히 0에서 멀어지는 경우를 보여 준다.
x, y 이외의 변수도 하나 넣어 sumResult가 그 변수의 값을 보존하는지 확인해 보자.
Comm.run c n σ는 반복을 펼칠 수 있는 연료 n을 받는다. 반복이 아닌 절은 연료를
소비하지 않고, while을 한 번 펼칠 때 남은 반복에 넘길 연료를 하나 줄인다.
/--
연료 해석기. Reynolds §2.4의 유한 근사와 같은 역할을 하는 실행 가능한 정의다.
`wh` 절만 연료를 쓴다. 연료가 남았으면 조건을 검사하고, 참이면 본체와 나머지 반복을
실행한다. 바깥 반복을 한 번 풀 때 나머지 반복의 연료는 하나 줄지만, 본체에는 현재 연료를
그대로 준다. 본체에 중첩된 반복이 있으면 그 반복도 유한하게 실행하기 위해서다.
연료가 바닥나면 `Flat.none`을 돌려준다. 이 값은 발산과 연료 부족을 구분하지 않는다.
`Comm.eval`과 달리 정의 전체를 계산할 수 있어 `#eval`과 `#guard`에서 사용할 수 있다.
-/defComm.run:CommV→ℕ→StateV→SigmaBotV|.assignve,_,σ=>Flat.some(σ[v:=⟦e⟧ₑσ])|.skip,_,σ=>Flat.someσ|.seqc₀c₁,n,σ=>Flat.bind(c₀.runnσ)(c₁.runn)|.itebc₀c₁,n,σ=>if⟦b⟧ᵇσthenc₀.runnσelsec₁.runnσ|.newvarvec,n,σ=>restorevσ(c.runn(σ[v:=⟦e⟧ₑσ]))|.wh__,0,_=>Flat.none|.whbc,n+1,σ=>if⟦b⟧ᵇσthenFlat.bind(c.run(n+1)σ)((Comm.whbc).runn)elseFlat.someσ
연료가 부족해 none이 나왔다고 해서 프로그램이 발산한다고 결론 내릴 수는 없다.
while x > 0 do x := x - 1을 x = 5에서 실행하면 연료 5로는 마지막 조건 검사를
마치지 못하지만, 연료 6에서는 x = 0인 상태를 얻는다. 반대로
while tt do skip은 어떤 유한 연료에서도 none이다.
Comm.run_le_succ는 연료를 늘릴 때 정보를 잃지 않는다는 정리다. 한 번
some σ'이 나오면 더 큰 연료에서도 같은 some σ'이 나온다. 평평한 순서에서는
종료 결과를 다른 값으로 바꾸는 것이 허용되지 않기 때문이다.
표시적 의미에서 종료 결과를 증명해도 해석기가 재현하지 못한다면 실행과 동떨어진 뜻이다.
반대로 해석기가 낸 결과가 표시적 의미와 다르면 테스트를 정리의 근거로 옮길 수 없다.
/--
**적합성(adequacy)** — 표시적 의미와 해석기가 일치한다.
왼쪽은 증명용이고 오른쪽은 실행용이다. `#guard`로 확인한 종료 결과는 건전성 방향을 통해
`⟦-⟧ᶜ`의 결과가 된다. 반대로 표시적 의미에서 증명한 종료 결과는 충분한 연료의 `run`에서
재현된다. 이 정리는 `Flat.none`에 관한 임의의 성질까지 옮긴다고 주장하지 않는다.
채점 연습이 아니다 — 이 정리의 증명은 `Comm.run_sound`와 `Comm.run_complete`에
의존한다. 두 보조 정리까지 함께 비우면 한 연습이 다른 미완성 증명에 의존하게 된다
(연습 독립성 원칙). 완전성 방향의 Scott 귀납법 적용을 완성본으로 읽는 것이 이 파일의
목적이다.
-/theoremComm.eval_eq_run{c:CommV}{σσ':StateV}:c.evalσ=Flat.someσ'↔∃n,c.runnσ=Flat.someσ':=byV:Type uinst✝:DecidableEqVc:CommVσ:StateVσ':StateV⊢ ⟦c⟧ᶜσ=Flat.someσ'↔∃n,c.runnσ=Flat.someσ'constructormpV:Type uinst✝:DecidableEqVc:CommVσ:StateVσ':StateV⊢ ⟦c⟧ᶜσ=Flat.someσ'→∃n,c.runnσ=Flat.someσ'mprV:Type uinst✝:DecidableEqVc:CommVσ:StateVσ':StateV⊢ (∃n,c.runnσ=Flat.someσ')→⟦c⟧ᶜσ=Flat.someσ'·mpV:Type uinst✝:DecidableEqVc:CommVσ:StateVσ':StateV⊢ ⟦c⟧ᶜσ=Flat.someσ'→∃n,c.runnσ=Flat.someσ'exactComm.run_completeAll goals completed! 🐙·mprV:Type uinst✝:DecidableEqVc:CommVσ:StateVσ':StateV⊢ (∃n,c.runnσ=Flat.someσ')→⟦c⟧ᶜσ=Flat.someσ'rintro⟨n,hn⟩mprV:Type uinst✝:DecidableEqVc:CommVσ:StateVσ':StateVn:ℕhn:c.runnσ=Flat.someσ'⊢ ⟦c⟧ᶜσ=Flat.someσ'exactComm.run_soundhnAll goals completed! 🐙
정리의 오른쪽은 “어떤 유한 연료에서 같은 종료 결과가 나온다”다. 따라서 run으로 확인한
종료 결과는 eval의 결과이고, eval에서 얻은 종료 결과도 충분한 연료로 실행된다.
이 진술은 eval σ = none과 “모든 연료에서 run이 none” 사이의 동치를 직접 적은
정리는 아니다. 현재 적합성 정리가 옮기는 범위는 some인 종료 결과다.
Reynolds/Exercises/Ch02/에서 sorry를 채운 뒤 다음 명령으로 상태를 확인한다.
lake exe grade --chapter 2
§2.5~§2.8과 책 연습 2.1~2.10의 실습은 뒤의 파일로 이어진다.
“§2.2” 같은 식별자는 해당 절의 형식화를 익히기 위해 저장소가 둔 연습이고,
“Prop 2.1”과 “Ex 2.4”는 책의 번호다. discrete가 붙은 명제 2.4 연습은 책의
일반 명제를 이산 입력에 특수화했다는 표시다.