4. 숫자 프로그램의 성질을 증명하기
자연수에 대해 계산하는 프로그램의 성질을 증명하기위해 보통 여러 계산과정이 어떤 방식으로 연관되어 있음을 보일수 있다. 주어진 함수가 재귀방식이면 대개 귀납적 방식으로 증명한다. 이때 알아야할 전술은 다음과 같다.
- induction 전술은 자연수 n에 대한 귀납적 증명을 한다. 증명을 두 부분으로 나누어, 우선 n이 0일때 증명하고자하는 성질이 성립함을 확인한다. 그 다음 p에 대한 성질로부터 S p에 대한 성질을 증명한다. p에 대한 가정을 귀납적 가정이라 부르고 IH로 시작하는 이름을 보통 붙인다. 이 전술은 0에 대한 목적 명제와 S p에 대한 목적 명제 두 개를 만든다.
- simpl 전술은 복잡한 인자를 갖는 재귀함수 호출을 해당하는 값으로 대체한다. 함수의 정의에 의해 이 값을 결정한다.
- discriminate 전술은 0 = S ...이나 true = false와 같이 선언하는 가정이 있을때 유용하다.
- injection 전술은 S x = S y 모양의 가정에서 x = y를 알아내는데 도움이 된다.
4.1 귀납적 증명
증명 예제를 살펴보자. 0부터 n개의 자연수들의 합이 잘 알려진 다항식을 이룬다는 것을 증명해보자.
Lemma sum_n_p : forall n, 2 * sum_n n + n = n * n.
induction n.
2 subgoals
============================
2 * sum_n 0 + 0 = 0 * 0
subgoal 2 is:
2 * sum_n (S n) + S n = S n * S n
새로 생성된 두 목적 명제에서, 첫번째 것은 n을 0으로 바꾸었고, 두번째는 n을 S n으로 바꾼것이다. 두번째 목적 명제에서 n에 대해 성립한다는 가정은 표시되어 있지 않다.
첫번째 목적 명제는 매우 쉽다. sum_n, 덧셈, 곱셈의 정의에 의하면 등식의 양쪽이 모두 0이기 때문이다. reflexivity 전술로 해결한다.
reflexivity.
이 전술을 쓰면 두번째 목적 명제만 남았고 콕 시스템에서 전체를 보여준다. 이제 귀납적 가정을 볼 수 있다.
1 subgoal
n : nat
IHn : 2 * sum_n n + n = n * n
============================
2 * sum_n (S n) + S n = S n * S n
여기에서 조금 생각할 필요가 있다. S n * S n을 n * n이 나타나는 식으로 펼칠수 있다. 그 다음 IHn으로 오른편에서 왼쪽 방향으로 다시 쓸 수 있다. assert 전술을 사용해 S n * S n과 새로운 값이 같다고 임시로 선언하고, ring 전술로 이 동등 선언이 사실 성립함을 보인다.
assert (SnSn : S n * S n = n * n + 2 * n + 1).
2 subgoals
n : nat
IHn : 2 * sum_n n + n = n * n
============================
S n * S n = n * n + 2 * n + 1
subgoal 2 is:
2 * sum_n (S n) + S n = S n * S n
ring.
1 subgoal
n : nat
IHn : 2 * sum_n n + n = n * n
SnSn : S n * S n = n * n + 2 * n + 1
============================
2 * sum_n (S n) + S n = S n * S n
rewrite SnSn.
1 subgoal
n : nat
IHn : 2 * sum_n n + n = n * n
SnSn : S n * S n = n * n + 2 * n + 1
============================
2 * sum_n (S n) + S n = n * n + 2 * n + 1
이번에는 귀납적 가정을 이용해서 n * n을 2 * sum_n n + n으로 다시 쓴다.
rewrite <- IHn.
1 subgoal
n : nat
IHn : 2 * sum_n n + n = n * n
SnSn : S n * S n = n * n + 2 * n + 1
============================
2 * sum_n (S n) + S n = 2 * sum_n n + n + 2 * n + 1
다음 단계에서 sum_n (S n)을 정의에 따라 펼쳐서 그 값을 sum_n n으로 표현해본다. simpl 전술을 사용하는데, 이것은 sum_n 뿐만 아니라 곱셈도 그 정의에 따라 펼치는 것을 시도한다.
sumpl.
1 subgoal
n : nat
IHn : 2 * sum_n n + n = n * n
SnSn : S n * S n = n * n + 2 * n + 1
============================
n + sum_n n + (n + sum_n n + 0) + S n =
sum_n n + (sum_n n + 0) + n + (n + (n + 0)) + 1
그 다음 단계에서 ring 전술을 사용해서 목적 명제의 등식이 덧셈의 결합성과 교환 규칙에 의해 성립함을 보인다.
ring.
Qed.
4.2 귀납에서 더 강력한 문장
재귀 함수가 몇 단계의 패턴 매칭을 사용한다면 연속적인 여러 개의 숫자들에 대해 성질을 기술하는 문장을 증명하는 것이 낫다. evenb 함수에 대한 증명의 예로 살펴보자.
Lemma evenb_p : forall n, evenb n = true -> exists x, n = 2 * x.
assert (Main: forall n, (evenb n = true -> exists x, n = 2 * x) /\
(evenb (S n) = true -> exists x, S n = 2 * x)).
여기에서 원래 목적 명제보다 더 강력한 문장을 증명해본다. 즉, n 뿐만 아니라 S n에 대해서도 성립함을 증명한다. 그 다음 이전처럼 귀납 방식으로 진행한다. 더 강력한 문장을 귀납 방식으로 증명하는 것은 재귀함수 증명에서 흔히 볼 수 있다.
induction n.
3 subgoals
============================
(evenb 0 = true -> exists x : nat, 0 = 2 * x) /\
(evenb 1 = true -> exists x : nat, 1 = 2 * x)
subgoal 2 is:
(evenb (S n) = true -> exists x : nat, S n = 2 * x) /\
(evenb (S (S n)) = true -> exists x : nat, S (S n) = 2 * x)
subgoal 3 is:
forall n : nat, evenb n = true -> exists x : nat, n = 2 * x
첫번째 목적 명제는 두 부분으로 나뉜다. 처음 부분은 결론에 나타난 존재 한정이 있는 문장이다. 전술 테이블을 보면 exists 전술로 어떤 값을 주면 된다. 이 경우 x의 값을 0으로 놓으면 0 = 2 * 0이 성립함을 쉽게 알 수 있다. 두번째 부분은 다르다. x의 값으로 놓을 수 있는 것이 없다. 하지만 evenb 1 = true는 성립하지 않는다. 왜냐하면 evenb 1은 false이고 false = true는 불가능하기 때문이다. 일관성이 없는 경우는 discriminate 전술을 사용한다.
split.
exists 0; ring.
simpl; intros H; discriminate H.
나머지 증명에 대해 자세히 설명하지 않는다. 독자 스스로 증명해볼 것을 권한다.
split.
destruct IHn as [_ IHn'].
exact IHn'.
simpl.
intros H.
destruct IHn as [IHn' _].
assert (H' : exists x, n = 2 * x).
apply IHn'.
exact H.
destruct H' as [x q].
exists (x + 1).
rewrite q.
ring.
가정 Main을 일단 증명하면 원래 증명하려고 했던 명제로 돌아갈수 있다. 이때 firstorder 전술로 증명을 마무리할 수 있다. 설명을 위해 다른 방법을 보여줄 것이다.
1 subgoal
Main : forall n : nat,
(evenb n = true -> exists x : nat, n = 2 * x) /\
(evenb (S n) = true -> exists x : nat, S n = 2 * x)
============================
forall n : nat, evenb n = true -> exists x : nat, n = 2 * x
intros n ev.
1 subgoal
Main : forall n : nat,
(evenb n = true -> exists x : nat, n = 2 * x) /\
(evenb (S n) = true -> exists x : nat, S n = 2 * x)
n : nat
ev : evenb n = true
============================
exists x : nat, n = 2 * x
여기서 지금까지 언급하지 않았던 콕 시스템의 한가지 편의 기능을 사용할 수 있다. 정리의 문장이 전칭 한정이거나 함축의 형태이면 이 정리를 마치 함수인 것 처럼 쓸 수 있다. 어떤 식에 적용하면 이것으로 사례화된 새로운 정리를 얻을 수 있다. 위의 예에서 Main 가정을 사용해서 n에 대해 사례화시킬수 있다.
destruct (Main n) as [H _].
apply H.
exact ev.
Qed.
덧셈에 다른 정의에 관한 연습문제
자연수에 대한 덧셈을 새롭게 정의할 수 있다.
Fixpoint add n m := match n with 0 => m | S p => add p (S m) end.
다음을 증명하시오.
forall n m, add n (S m) = S (add n m)
forall n m, add (S n) m = S (add n m)
forall n m, add n m = n + m
새로운 연습문제를 증명할때 이전에 증명한 보조 정리를 사용할 수 있다.
홀수의 합에 대한 연습문제
앞에서부터 n개의 홀수를 모두 더한 합은 다음 함수로 정의한다.
Fixpoint sum_odd_n (n:nat) : nat :=
match n with 0 => 0 | S p => 1 + 2 * p + sum_odd_n p end.
다음 문장을 증명하시오.
forall n:nat, sum_odd_n n = n * n
===
번역
(1장)
Expressions : 식
Formulas : 식
infix : 인픽스
Logical Formulas : 논리식
Symbolic Computation : 기호 계산
Terms : 텀
Well-formed : 제대로 된 형태
(2장)
Keyword : 키워드
Boolean : 부울
Conjunction : 곱
Disjunction : 합
Negation : 부정
Construct : 구문
Pattern-matching : 패턴 매칭
Recursivity : 재귀 형태
Structural Recursion : 구조적 재귀
Subterm : 부분식
Sublist : 부분리스트
(3장)
Fact: 사실 명제
Theorem: 정리
Implication : 함축
Predicate : (술어) 명제
Rewriting : 다시쓰기
Equality : 등식
Goal : 목적 명제
Tactic : 전술
Hypothesis : 가정
Universal Quantification : 전칭 한정
Transitive : 추이적
Instantiation : 사례화
Associativity : 결합성
(4장)
Proof by induction : 귀납적 증명
Existential : 존재
Lemma : 보조 정리
'Coq 증명기' 카테고리의 다른 글
[Coq 증명기] 콕, 빨리 빨리: 6장 새로운 데이터 타입 정의하기 (0) | 2014.08.09 |
---|---|
[Coq 증명기] 콕, 빨리 빨리: 5장 리스트 프로그램의 성질을 증명하기 (0) | 2014.08.09 |
[Coq 증명기] 콕, 빨리 빨리: 3장 계산식과 논리식 (0) | 2014.08.09 |
[Coq 증명기] 콕, 빨리 빨리: 2장 콕으로 프로그래밍하기 (0) | 2014.08.09 |
[Coq 증명기] 콕, 빨리 빨리: 1장 계산식과 논리식 (0) | 2014.08.09 |