Lean 4로 정리 증명하기

4. 한정자와 상등🔗

지난 장에서는 명제 연결사가 포함된 진술의 증명을 구성하는 방법을 소개했습니다. 이 장에서는 논리적 구성의 범위를 넓혀 전칭 한정자와 존재 한정자, 그리고 상등 관계까지 다룹니다.

4.1. 전칭 한정자🔗

α가 임의의 타입이라면, α 위의 단항 술어 p를 α → Prop 타입의 객체로 표현할 수 있다는 점에 주목하십시오. 이 경우 x : α가 주어지면, p x는 p가 x에 대해 성립한다는 주장을 나타냅니다. 마찬가지로, 객체 r : α → α → Prop는 α 위의 이항 관계를 나타냅니다. x y : α가 주어지면, r x y는 x가 y와 관계되어 있다는 주장을 나타냅니다.

전칭 한정자 ∀ x : α, p x는 “모든 x : α에 대해 p x가 성립한다”는 주장을 나타내도록 되어 있습니다. 명제 연결사와 마찬가지로, 자연 연역 체계에서 “forall”은 도입 규칙과 제거 규칙의 지배를 받습니다. 비형식적으로 말하면, 도입 규칙은 다음과 같습니다:

x : α가 임의인 맥락에서 p x의 증명이 주어지면, 증명 ∀ x : α, p x를 얻습니다.

제거 규칙은 다음과 같습니다:

∀ x : α, p x의 증명과 임의의 항 t : α가 주어지면 p t의 증명을 얻습니다.

함의의 경우와 마찬가지로, 이제 명제-타입 대응(propositions-as-types) 해석이 작용합니다. 의존 화살표 타입의 도입 규칙과 제거 규칙을 떠올려 보십시오:

x : α가 임의인 문맥에서 β x 타입의 항 t가 주어지면, (fun x : α => t) : (x : α) → β x를 얻습니다.

제거 규칙은 다음과 같습니다.

항 s : (x : α) → β x와 임의의 항 t : α가 주어지면 s t : β t입니다.

p x의 타입이 Prop인 경우, (x : α) → β x를 ∀ x : α, p x로 바꾸면 이 규칙들을 전칭 한정자가 포함된 증명을 구성하는 올바른 규칙으로 읽을 수 있습니다.

따라서 구성 계산법(Calculus of Constructions)은 이러한 방식으로 의존 화살표 타입을 forall 표현식과 동일시합니다. p가 임의의 표현식이라면, ∀ x : α, p는 (x : α) → p의 대체 표기법에 지나지 않습니다. 이는 p가 명제인 경우에는 전자가 후자보다 더 자연스럽다는 생각에서 비롯된 것입니다. 일반적으로 표현식 p는 x : α에 의존합니다. 일반적인 함수 공간의 경우, α → β를 β가 x에 의존하지 않는 (x : α) → β의 특수한 경우로 해석할 수 있었다는 점을 떠올려 보십시오. 마찬가지로, 명제 사이의 함의 p → q도 표현식 q가 x에 의존하지 않는 ∀ x : p, q의 특수한 경우로 생각할 수 있습니다.

다음은 명제-타입 대응이 실제로 어떻게 적용되는지 보여 주는 예입니다.

example (α : Type) (p q : α → Prop) : (∀ x : α, p x ∧ q x) → ∀ y : α, p y := fun h : ∀ x : α, p x ∧ q x => fun y : α => show p y from (h y).left

표기법상의 관례로, 전칭 한정자에는 가능한 한 가장 넓은 범위를 부여합니다. 따라서 위 예제에서 x에 대한 한정자의 범위를 가설로 제한하려면 괄호가 필요합니다. ∀ y : α, p y를 증명하는 표준적인 방법은 임의의 y를 취하여 p y를 증명하는 것입니다. 이것이 도입 규칙입니다. 이제 h가 ∀ x : α, p x ∧ q x 타입을 가진다고 하면, 표현식 h y는 p y ∧ q y 타입을 가집니다. 이것이 제거 규칙입니다. 왼쪽 연언지를 취하면 원하는 결론인 p y를 얻습니다.

속박 변수의 이름을 바꾼 것만 다른 표현식들은 동치로 간주된다는 점을 기억하십시오. 따라서 예를 들어, 가설과 결론 모두에 같은 변수 x를 사용하고, 증명에서는 이를 다른 변수 z로 인스턴스화할 수도 있었습니다:

example (α : Type) (p q : α → Prop) : (∀ x : α, p x ∧ q x) → ∀ x : α, p x := fun h : ∀ x : α, p x ∧ q x => fun z : α => show p z from And.left (h z)

또 다른 예로, 관계 r이 추이적이라는 사실은 다음과 같이 표현할 수 있습니다:

variable (α : Type) (r : α → α → Prop) variable (trans_r : ∀ x y z, r x y → r y z → r x z) variable (a b c : α) variable (hab : r a b) (hbc : r b c) trans_r : ∀ (x y z : α), r x y → r y z → r x z#check trans_r
trans_r : ∀ (x y z : α), r x y → r y z → r x z
trans_r a b c : r a b → r b c → r a c#check trans_r a b c
trans_r a b c : r a b → r b c → r a c
trans_r a b c hab : r b c → r a c#check trans_r a b c hab
trans_r a b c hab : r b c → r a c
trans_r a b c hab hbc : r a c#check trans_r a b c hab hbc
trans_r a b c hab hbc : r a c

여기서 어떤 일이 일어나고 있는지 생각해 보십시오. trans_r을 값 a b c로 인스턴스화하면, 결국 r a b → r b c → r a c의 증명을 얻게 됩니다. 이것을 “가설” hab : r a b에 적용하면, 함의 r b c → r a c의 증명을 얻습니다. 마지막으로, 이것을 가설 hbc에 적용하면 결론 r a c의 증명을 얻습니다.

이와 같은 상황에서는 인수 a b c를 hab hbc로부터 추론할 수 있는데도 일일이 제공해야 하므로 번거로울 수 있습니다. 그러한 이유로 이 인수들을 암시적으로 만드는 것이 일반적입니다:

variable (α : Type) (r : α → α → Prop) variable (trans_r : ∀ {x y z}, r x y → r y z → r x z) variable (a b c : α) variable (hab : r a b) (hbc : r b c) trans_r : r ?m.4 ?m.5 → r ?m.5 ?m.6 → r ?m.4 ?m.6#check trans_r
trans_r : r ?m.4 ?m.5 → r ?m.5 ?m.6 → r ?m.4 ?m.6
trans_r hab : r b ?m.6 → r a ?m.6#check trans_r hab
trans_r hab : r b ?m.6 → r a ?m.6
trans_r hab hbc : r a c#check trans_r hab hbc
trans_r hab hbc : r a c

이 방식의 장점은 r a c의 증명으로 trans_r hab hbc라고 간단히 쓸 수 있다는 것입니다. 단점은 trans_r와 trans_r hab 표현식에서 인자의 타입을 추론하는 데 필요한 정보가 Lean에 충분하지 않다는 것입니다. 첫 번째 #check 명령의 출력은 r ?m.1 ?m.2 → r ?m.2 ?m.3 → r ?m.1 ?m.3이며, 이는 이 경우 암시적 인자가 지정되지 않았음을 나타냅니다.

다음은 동치 관계를 사용하여 기초적인 추론을 수행하는 방법을 보여 주는 예입니다:

variable (α : Type) (r : α → α → Prop) variable (refl_r : ∀ x, r x x) variable (symm_r : ∀ {x y}, r x y → r y x) variable (trans_r : ∀ {x y z}, r x y → r y z → r x z) example (a b c d : α) (hab : r a b) (hcb : r c b) (hcd : r c d) : r a d := trans_r (trans_r hab (symm_r hcb)) hcd

전칭 한정자 사용에 익숙해지려면 이 절 끝에 있는 연습 문제 몇 개를 풀어 보십시오.

Prop을 다른 타입과 구별 짓는 것은 바로 의존 화살표 타입, 특히 전칭 한정자에 대한 타이핑 규칙입니다. α : Sort i와 β : Sort j가 있다고 가정합시다. 여기서 표현식 β는 변수 x : α에 의존할 수 있습니다. 그러면 (x : α) → β는 Sort (imax i j)의 원소입니다. 여기서 imax i j는 j가 0이 아니면 i와 j 중 최댓값이고, 그렇지 않으면 0입니다.

개념은 다음과 같습니다. j가 0이 아니면 (x : α) → β는 Sort (max i j)의 원소입니다. 다시 말해, α에서 β로 가는 의존 함수의 타입은 i와 j 중 최댓값을 인덱스로 갖는 유니버스에 “속합니다”. 그런데 β가 Sort 0에 속한다고, 즉 Prop의 원소라고 가정해 봅시다. 이 경우 α가 어느 타입 유니버스에 속하든 관계없이 (x : α) → β 역시 Sort 0의 원소입니다. 다시 말해, β가 α에 의존하는 명제라면 ∀ x : α, β 역시 명제입니다. 이는 Prop을 데이터가 아닌 명제의 타입으로 보는 해석을 반영하며, Prop을 비서술적(impredicative)으로 만드는 것이 바로 이 성질입니다.

“서술적(predicative)”이라는 용어는 20세기로 넘어갈 무렵의 기초론적 발전에서 유래합니다. 당시 푸앵카레와 러셀 같은 논리학자들은 집합론의 역설이 “악순환”에서 비롯된다고 보았습니다. 이 악순환은 정의하려는 바로 그 성질을 항목으로 포함하는 모임에 대해 한정하여 그 성질을 정의할 때 생깁니다. α가 임의의 타입이면 α 위의 모든 술어로 이루어진 타입 α → Prop(“α의 멱타입”)을 만들 수 있다는 점에 주목하십시오. Prop이 비술어적(impredicative)이라는 것은 α → Prop에 대해 양화하는 명제를 만들 수 있다는 뜻입니다. 특히 α 위의 모든 술어에 대해 양화하여 α 위의 술어를 정의할 수 있습니다. 이것이 바로 한때 문제가 있다고 여겨졌던 종류의 순환성입니다.

4.2. 상등🔗

이제 Lean 라이브러리에 정의된 가장 기본적인 관계 중 하나인 상등 관계를 살펴봅시다. 귀납적 타입에 관한 장에서는 Lean 논리 체계의 기본 요소로부터 상등이 어떻게 정의되는지 설명하겠습니다. 그에 앞서 여기서는 상등을 사용하는 방법을 설명합니다.

물론, 상등의 근본적인 성질은 그것이 동치 관계라는 점입니다:

Eq.refl.{u_1} {α : Sort u_1} (a : α) : a = a#check Eq.refl
Eq.refl.{u_1} {α : Sort u_1} (a : α) : a = a
Eq.symm.{u} {α : Sort u} {a b : α} (h : a = b) : b = a#check Eq.symm
Eq.symm.{u} {α : Sort u} {a b : α} (h : a = b) : b = a
Eq.trans.{u} {α : Sort u} {a b c : α} (h₁ : a = b) (h₂ : b = c) : a = c#check Eq.trans
Eq.trans.{u} {α : Sort u} {a b c : α} (h₁ : a = b) (h₂ : b = c) : a = c

Lean에게 암시적 인자(여기서는 메타변수로 표시됩니다)를 삽입하지 않도록 지시하면 출력을 더 읽기 쉽게 만들 수 있습니다.

universe u @Eq.refl : ∀ {α : Sort u} (a : α), a = a#check @Eq.refl.{u}
@Eq.refl : ∀ {α : Sort u} (a : α), a = a
@Eq.symm : ∀ {α : Sort u} {a b : α}, a = b → b = a#check @Eq.symm.{u}
@Eq.symm : ∀ {α : Sort u} {a b : α}, a = b → b = a
@Eq.trans : ∀ {α : Sort u} {a b c : α}, a = b → b = c → a = c#check @Eq.trans.{u}
@Eq.trans : ∀ {α : Sort u} {a b c : α}, a = b → b = c → a = c

.{u} 표기는 Lean에게 해당 상수들을 유니버스 u에서 인스턴스화하도록 지시합니다.

따라서 예를 들어, 이전 절의 예제를 상등 관계에 맞게 특수화할 수 있습니다:

variable (α : Type) (a b c d : α) variable (hab : a = b) (hcb : c = b) (hcd : c = d) example : a = d := Eq.trans (Eq.trans hab (Eq.symm hcb)) hcd

사영 표기법을 사용할 수도 있습니다:

example : a = d := (hab.trans hcb.symm).trans hcd

반사성(reflexivity)은 보기보다 강력합니다. 구성 계산법(Calculus of Constructions)의 항은 계산적 해석을 가지며, 논리 체계는 공통된 축약형(reduct)을 갖는 항들을 같은 것으로 취급한다는 점을 상기하십시오. 그 결과, 자명하지 않은 몇몇 항등식도 반사성으로 증명할 수 있습니다:

variable (α β : Type) example (f : α → β) (a : α) : (fun x => f x) a = f a := Eq.refl _ example (a : α) (b : β) : (a, b).1 = a := Eq.refl _ example : 2 + 3 = 5 := Eq.refl _

프레임워크의 이 특성은 매우 중요하기 때문에, 라이브러리에서는 Eq.refl _의 표기법으로 rfl을 정의합니다:

example (f : α → β) (a : α) : (fun x => f x) a = f a := rfl example (a : α) (b : β) : (a, b).1 = a := rfl example : 2 + 3 = 5 := rfl

그러나 상등은 단순한 동치 관계보다 훨씬 더 많은 것을 담고 있습니다. 상등은 모든 주장이 이 동치를 존중한다는 중요한 성질을 가지는데, 이는 같은 표현식끼리는 진릿값을 바꾸지 않고 서로 치환할 수 있다는 의미입니다. 다시 말해, h1 : a = b와 h2 : p a가 주어지면 치환을 이용하여 p b에 대한 증명을 구성할 수 있습니다: Eq.subst h1 h2.

example (α : Type) (a b : α) (p : α → Prop) (h1 : a = b) (h2 : p a) : p b := Eq.subst h1 h2 example (α : Type) (a b : α) (p : α → Prop) (h1 : a = b) (h2 : p a) : p b := h1 ▸ h2

두 번째 표기에 쓰인 삼각형은 Eq.subst와 Eq.symm을 바탕으로 만든 매크로이며, \t를 입력하여 쓸 수 있습니다.

Eq.subst 규칙은 더 명시적인 치환을 수행하는 다음 보조 규칙들을 정의하는 데 사용됩니다. 이 규칙들은 적용 형태의 항, 즉 s t 형태의 항을 다루도록 설계되었습니다. 구체적으로, congrArg는 인자를 치환하는 데, congrFun은 적용되는 항을 치환하는 데, congr는 둘 다를 한꺼번에 치환하는 데 사용할 수 있습니다.

variable (α : Type) variable (a b : α) variable (f g : α → Nat) variable (h₁ : a = b) variable (h₂ : f = g) example : f a = f b := congrArg f h₁ example : f a = g a := congrFun h₂ a example : f a = g b := congr h₂ h₁

Lean의 라이브러리에는 다음과 같이 흔히 쓰이는 항등식이 많이 들어 있습니다.

variable (a b c : Nat) example : a + 0 = a := Nat.add_zero a example : 0 + a = a := Nat.zero_add a example : a * 1 = a := Nat.mul_one a example : 1 * a = a := Nat.one_mul a example : a + b = b + a := Nat.add_comm a b example : a + b + c = a + (b + c) := Nat.add_assoc a b c example : a * b = b * a := Nat.mul_comm a b example : a * b * c = a * (b * c) := Nat.mul_assoc a b c example : a * (b + c) = a * b + a * c := Nat.mul_add a b c example : a * (b + c) = a * b + a * c := Nat.left_distrib a b c example : (a + b) * c = a * c + b * c := Nat.add_mul a b c example : (a + b) * c = a * c + b * c := Nat.right_distrib a b c

Nat.mul_add와 Nat.add_mul은 각각 Nat.left_distrib와 Nat.right_distrib의 다른 이름이라는 점에 유의하십시오. 위의 성질들은 자연수(타입 Nat)에 대해 기술된 것입니다.

다음은 자연수에서 치환을 결합법칙 및 분배법칙과 함께 사용하는 계산의 예입니다.

example (x y : Nat) : (x + y) * (x + y) = x * x + y * x + x * y + y * y := have h1 : (x + y) * (x + y) = (x + y) * x + (x + y) * y := Nat.mul_add (x + y) x y have h2 : (x + y) * (x + y) = x * x + y * x + (x * y + y * y) := (Nat.add_mul x y x) ▸ (Nat.add_mul x y y) ▸ h1 h2.trans (Nat.add_assoc (x * x + y * x) (x * y) (y * y)).symm

Eq.subst의 두 번째 암시적 매개변수는 치환이 일어날 문맥을 제공하며, 그 타입이 α → Prop이라는 점에 주목하십시오. 따라서 이 술어를 추론하려면 고차 단일화(higher-order unification)의 한 사례를 풀어야 합니다. 가장 일반적인 경우에 고차 단일자가 존재하는지 판정하는 문제는 결정 불가능하며, Lean은 기껏해야 이 문제에 대해 불완전하고 근사적인 해만을 제공할 수 있습니다. 그 결과, Eq.subst가 항상 원하는 대로 동작하지는 않습니다. 매크로 h ▸ e는 이 암시적 매개변수를 계산하는 데 더 효과적인 휴리스틱을 사용하며, Eq.subst를 적용하면 실패하는 상황에서도 성공하는 경우가 많습니다.

등식 추론은 매우 흔하고 중요하기 때문에, Lean은 이를 더 효과적으로 수행할 수 있는 여러 메커니즘을 제공합니다. 다음 절에서는 계산적 증명을 더 자연스럽고 명료하게 작성할 수 있게 해 주는 구문을 소개합니다. 하지만 더 중요한 것은, 등식 추론이 항 재작성기(term rewriter), 단순화기(simplifier), 그리고 그 밖의 여러 자동화 도구에 의해 지원된다는 점입니다. 항 재작성기와 단순화기는 다음 절에서 간략히 설명하고, 이어서 다음 장에서 더 자세히 설명합니다.

4.3. 계산적 증명🔗

계산적 증명은 상등의 추이성과 같은 기본 원리에 의해 합성되도록 의도된 중간 결과들의 사슬일 뿐입니다. Lean에서 계산적 증명은 키워드 calc로 시작하며, 다음과 같은 구문을 가집니다:

calc
  <expr>_0  'op_1'  <expr>_1  ':='  <proof>_1
  '_'       'op_2'  <expr>_2  ':='  <proof>_2
  ...
  '_'       'op_n'  <expr>_n  ':='  <proof>_n

calc 관계들은 모두 들여쓰기가 같다는 점에 유의하십시오. 각 <proof>_i는 <expr>_{i-1} op_i <expr>_i에 대한 증명입니다.

첫 번째 관계(<expr>_0 바로 뒤)에서도 _를 사용할 수 있으며, 이는 관계/증명 쌍들을 가지런히 정렬하는 데 유용합니다:

calc <expr>_0
    '_' 'op_1' <expr>_1 ':=' <proof>_1
    '_' 'op_2' <expr>_2 ':=' <proof>_2
    ...
    '_' 'op_n' <expr>_n ':=' <proof>_n

다음은 예시입니다:

variable (a b c d e : Nat) theorem T (h1 : a = b) (h2 : b = c + 1) (h3 : c = d) (h4 : e = 1 + d) : a = e := calc a = b := h1 _ = c + 1 := h2 _ = d + 1 := congrArg Nat.succ h3 _ = 1 + d := Nat.add_comm d 1 _ = e := Eq.symm h4

이러한 증명 작성 방식은 simp 및 rw 택틱과 함께 사용할 때 가장 효과적입니다. 이 택틱들은 다음 장에서 더 자세히 다룹니다. 예를 들어, 재작성(rewrite)을 수행하는 rw를 사용하면 위의 증명을 다음과 같이 작성할 수 있습니다.

theorem T (h1 : a = b) (h2 : b = c + 1) (h3 : c = d) (h4 : e = 1 + d) : a = e := calc a = b := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ a = b All goals completed! 🐙 _ = c + 1 := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ b = c + 1 All goals completed! 🐙 _ = d + 1 := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ c + 1 = d + 1 All goals completed! 🐙 _ = 1 + d := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ d + 1 = 1 + d All goals completed! 🐙 _ = e := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ 1 + d = e All goals completed! 🐙

본질적으로 rw 택틱은 주어진 상등(가설, 정리 이름 또는 복잡한 항일 수 있습니다)을 사용하여 목표를 "재작성"합니다. 그렇게 하여 목표가 항등식 t = t로 환원되면, 택틱은 반사성을 적용하여 이를 증명합니다.

재작성은 순차적으로 적용할 수 있으므로 위의 증명을 다음과 같이 줄일 수 있습니다.

theorem T (h1 : a = b) (h2 : b = c + 1) (h3 : c = d) (h4 : e = 1 + d) : a = e := calc a = d + 1 := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ a = d + 1 All goals completed! 🐙 _ = 1 + d := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ d + 1 = 1 + d All goals completed! 🐙 _ = e := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ 1 + d = e All goals completed! 🐙

혹은 이렇게 쓸 수도 있습니다:

theorem T (h1 : a = b) (h2 : b = c + 1) (h3 : c = d) (h4 : e = 1 + d) : a = e := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ a = e All goals completed! 🐙

반면 simp 택틱은 주어진 항등식들을 항 안에서 적용할 수 있는 곳이면 어디에서든 순서에 상관없이 반복적으로 적용하여 목표를 다시 씁니다. 또한 이 택틱은 시스템에 미리 선언되어 있던 다른 규칙들도 사용하며, 무한 루프에 빠지지 않도록 교환법칙을 적절히 적용합니다. 그 결과, 이 정리를 다음과 같이 증명할 수도 있습니다:

theorem T (h1 : a = b) (h2 : b = c + 1) (h3 : c = d) (h4 : e = 1 + d) : a = e := a:Natb:Natc:Natd:Nate:Nath1:a = bh2:b = c + 1h3:c = dh4:e = 1 + d⊢ a = e All goals completed! 🐙

다음 장에서는 rw와 simp의 여러 변형을 다루겠습니다.

calc 명령은 어떤 형태로든 추이성을 지원하는 관계라면 무엇이든 사용할 수 있도록 설정할 수 있습니다. 심지어 서로 다른 관계를 조합할 수도 있습니다.

variable (a b c d : Nat) example (h1 : a = b) (h2 : b ≤ c) (h3 : c + 1 < d) : a < d := calc a = b := h1 _ < b + 1 := Nat.lt_succ_self b _ ≤ c + 1 := Nat.succ_le_succ h2 _ < d := h3

Trans 타입 클래스의 새 인스턴스를 추가하여 calc에 새로운 추이성 정리를 “가르칠” 수 있습니다. 타입 클래스는 뒤에서 소개하지만, 다음의 작은 예제는 새 Trans 인스턴스를 사용하여 calc 표기법을 확장하는 방법을 보여 줍니다.

def divides (x y : Nat) : Prop := ∃ k, k*x = y Definition `divides_trans` is a proposition; use `theorem` instead of `def` Note: This linter can be disabled with `set_option linter.defProp false`def divides_trans (h₁ : divides x y) (h₂ : divides y z) : divides x z := let ⟨k₁, d₁⟩ := h₁ let ⟨k₂, d₂⟩ := h₂ ⟨k₁ * k₂, x:Naty:Natz:Nath₁:divides x yh₂:divides y zk₁:Natd₁:k₁ * x = yk₂:Natd₂:k₂ * y = z⊢ k₁ * k₂ * x = z All goals completed! 🐙⟩ Definition `divides_mul` is a proposition; use `theorem` instead of `def` Note: This linter can be disabled with `set_option linter.defProp false`def divides_mul (x : Nat) (k : Nat) : divides x (k*x) := ⟨k, rfl⟩ instance : Trans divides divides divides where trans := divides_trans example (h₁ : divides x y) (h₂ : y = z) : divides x (2*z) := calc divides x y := h₁ _ = z := h₂ divides _ (2*z) := divides_mul .. infix:50 " | " => divides example (h₁ : divides x y) (h₂ : y = z) : divides x (2*z) := calc x | y := h₁ _ = z := h₂ _ | 2*z := divides_mul ..

위 예제는 관계에 대한 중위 표기법이 없더라도 calc를 사용할 수 있다는 점도 분명히 보여 줍니다. Lean에는 이미 나누어떨어짐을 나타내는 표준 유니코드 표기법(\dvd 또는 \mid로 입력할 수 있는 ∣를 사용합니다)이 포함되어 있으므로, 위 예제에서는 충돌을 피하기 위해 일반 세로 막대를 사용합니다. 실제로는 match ... with 표현식에서 사용되는 ASCII |와 혼동될 위험이 있으므로 좋은 방법이 아닙니다.

calc를 사용하면 앞 절의 증명을 더 자연스럽고 명료하게 작성할 수 있습니다.

variable (x y : Nat) example : (x + y) * (x + y) = x * x + y * x + x * y + y * y := calc (x + y) * (x + y) = (x + y) * x + (x + y) * y := x:Naty:Nat⊢ (x + y) * (x + y) = (x + y) * x + (x + y) * y All goals completed! 🐙 _ = x * x + y * x + (x + y) * y := x:Naty:Nat⊢ (x + y) * x + (x + y) * y = x * x + y * x + (x + y) * y All goals completed! 🐙 _ = x * x + y * x + (x * y + y * y) := x:Naty:Nat⊢ x * x + y * x + (x + y) * y = x * x + y * x + (x * y + y * y) All goals completed! 🐙 _ = x * x + y * x + x * y + y * y := x:Naty:Nat⊢ x * x + y * x + (x * y + y * y) = x * x + y * x + x * y + y * y All goals completed! 🐙

여기서는 calc의 다른 표기법도 고려해 볼 만합니다. 첫 번째 표현식이 이만큼 많은 공간을 차지할 때는 첫 번째 관계에 _를 사용하면 모든 관계가 자연스럽게 정렬됩니다:

variable (x y : Nat) example : (x + y) * (x + y) = x * x + y * x + x * y + y * y := calc (x + y) * (x + y) _ = (x + y) * x + (x + y) * y := x:Naty:Nat⊢ (x + y) * (x + y) = (x + y) * x + (x + y) * y All goals completed! 🐙 _ = x * x + y * x + (x + y) * y := x:Naty:Nat⊢ (x + y) * x + (x + y) * y = x * x + y * x + (x + y) * y All goals completed! 🐙 _ = x * x + y * x + (x * y + y * y) := x:Naty:Nat⊢ x * x + y * x + (x + y) * y = x * x + y * x + (x * y + y * y) All goals completed! 🐙 _ = x * x + y * x + x * y + y * y := x:Naty:Nat⊢ x * x + y * x + (x * y + y * y) = x * x + y * x + x * y + y * y All goals completed! 🐙

여기서 Nat.add_assoc 앞의 왼쪽 화살표는 rewrite가 해당 항등식을 반대 방향으로 사용하도록 지시합니다. (이 화살표는 \l로 입력하거나, 이에 해당하는 ASCII 표기인 <-를 사용할 수 있습니다.) 간결함이 목표라면 rw와 simp 모두 단독으로 이 작업을 해낼 수 있습니다:

variable (x y : Nat) example : (x + y) * (x + y) = x * x + y * x + x * y + y * y := x:Naty:Nat⊢ (x + y) * (x + y) = x * x + y * x + x * y + y * y All goals completed! 🐙 example : (x + y) * (x + y) = x * x + y * x + x * y + y * y := x:Naty:Nat⊢ (x + y) * (x + y) = x * x + y * x + x * y + y * y All goals completed! 🐙

4.4. 존재 한정자🔗

마지막으로, exists x : α, p x 또는 ∃ x : α, p x로 쓸 수 있는 존재 한정자를 살펴봅시다. 두 형태 모두 실제로는 Lean 라이브러리에 정의된 더 장황한 표현식 Exists (fun x : α => p x)를 표기상 편리하게 줄여 쓴 것입니다.

이제는 예상하시겠지만, 라이브러리에는 도입 규칙과 제거 규칙이 모두 포함되어 있습니다. 도입 규칙은 간단합니다. ∃ x : α, p x를 증명하려면 적절한 항 t와 p t의 증명을 제공하면 충분합니다. 다음은 몇 가지 예입니다.

example : ∃ x : Nat, x > 0 := have h : 1 > 0 := Nat.zero_lt_succ 0 Exists.intro 1 h example (x : Nat) (h : x > 0) : ∃ y, y < x := Exists.intro 0 h example (x y z : Nat) (hxy : x < y) (hyz : y < z) : ∃ w, x < w ∧ w < z := Exists.intro y (And.intro hxy hyz) @Exists.intro : ∀ {α : Sort u_1} {p : α → Prop} (w : α), p w → Exists p#check @Exists.intro
@Exists.intro : ∀ {α : Sort u_1} {p : α → Prop} (w : α), p w → Exists p

문맥에서 타입이 분명할 때는 Exists.intro t h 대신 익명 생성자 표기법 ⟨t, h⟩를 사용할 수 있습니다.

example : ∃ x : Nat, x > 0 := have h : 1 > 0 := Nat.zero_lt_succ 0 ⟨1, h⟩ example (x : Nat) (h : x > 0) : ∃ y, y < x := ⟨0, h⟩ example (x y z : Nat) (hxy : x < y) (hyz : y < z) : ∃ w, x < w ∧ w < z := ⟨y, hxy, hyz⟩

Exists.intro에는 암시적 인자가 있다는 점에 주목하십시오. Lean은 결론 ∃ x, p x에 있는 술어 p : α → Prop를 추론해야 합니다. 이는 결코 간단한 일이 아닙니다. 예를 들어 hg : g 0 0 = 0이 있을 때 Exists.intro 0 hg라고 쓰면, 술어 p에 가능한 값은 여러 가지이며, 각각 정리 ∃ x, g x x = x, ∃ x, g x x = 0, ∃ x, g x 0 = x 등에 대응합니다. Lean은 문맥을 사용하여 이 중 어느 것이 적절한지 추론합니다. 이는 다음 예제에서 확인할 수 있습니다. 이 예제에서는 Lean의 pretty-printer가 암시적 인자를 표시하도록 옵션 pp.explicit를 true로 설정합니다.

variable (g : Nat → Nat → Nat) theorem gex1 (hg : g 0 0 = 0) : ∃ x, g x x = x := ⟨0, hg⟩ theorem gex2 (hg : g 0 0 = 0) : ∃ x, g x 0 = x := ⟨0, hg⟩ theorem gex3 (hg : g 0 0 = 0) : ∃ x, g 0 0 = x := ⟨0, hg⟩ theorem gex4 (hg : g 0 0 = 0) : ∃ x, g x x = 0 := ⟨0, hg⟩ set_option pp.explicit true -- display implicit arguments theorem gex1 : ∀ (g : Nat → Nat → Nat), @Eq Nat (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) → @Exists Nat fun x => @Eq Nat (g x x) x := fun g hg => @Exists.intro Nat (fun x => @Eq Nat (g x x) x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg#print gex1
theorem gex1 : ∀ (g : Nat → Nat → Nat),
  @Eq Nat
      (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))
        (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
      (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) →
    @Exists Nat fun x => @Eq Nat (g x x) x :=
fun g hg => @Exists.intro Nat (fun x => @Eq Nat (g x x) x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg
theorem gex2 : ∀ (g : Nat → Nat → Nat), @Eq Nat (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) → @Exists Nat fun x => @Eq Nat (g x (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) x := fun g hg => @Exists.intro Nat (fun x => @Eq Nat (g x (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg#print gex2
theorem gex2 : ∀ (g : Nat → Nat → Nat),
  @Eq Nat
      (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))
        (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
      (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) →
    @Exists Nat fun x => @Eq Nat (g x (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) x :=
fun g hg =>
  @Exists.intro Nat (fun x => @Eq Nat (g x (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) x)
    (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg
theorem gex3 : ∀ (g : Nat → Nat → Nat), @Eq Nat (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) → @Exists Nat fun x => @Eq Nat (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) x := fun g hg => @Exists.intro Nat (fun x => @Eq Nat (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg#print gex3
theorem gex3 : ∀ (g : Nat → Nat → Nat),
  @Eq Nat
      (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))
        (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
      (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) →
    @Exists Nat fun x =>
      @Eq Nat
        (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))
          (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
        x :=
fun g hg =>
  @Exists.intro Nat
    (fun x =>
      @Eq Nat
        (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))
          (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
        x)
    (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg
theorem gex4 : ∀ (g : Nat → Nat → Nat), @Eq Nat (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) → @Exists Nat fun x => @Eq Nat (g x x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) := fun g hg => @Exists.intro Nat (fun x => @Eq Nat (g x x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg#print gex4
theorem gex4 : ∀ (g : Nat → Nat → Nat),
  @Eq Nat
      (g (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0)))
        (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
      (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) →
    @Exists Nat fun x => @Eq Nat (g x x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) :=
fun g hg =>
  @Exists.intro Nat (fun x => @Eq Nat (g x x) (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))))
    (@OfNat.ofNat Nat (nat_lit 0) (instOfNatNat (nat_lit 0))) hg

Exists.intro는 주장의 본문에 대한 증거를 숨기므로, 정보를 숨기는 연산으로 볼 수 있습니다. 존재 제거 규칙인 Exists.elim은 그 반대 연산을 수행합니다. 이 규칙을 사용하면 임의의 값 w에 대해 p w로부터 q가 따라 나옴을 보임으로써 ∃ x : α, p x로부터 명제 q를 증명할 수 있습니다. 대략적으로 말하면, p x를 만족하는 x가 존재한다는 것을 알고 있으므로 그것에 이름을 붙일 수 있습니다. 예를 들어 w라고 부릅시다. q가 w를 언급하지 않는다면, p w로부터 q가 따라 나옴을 보이는 것은 그러한 x가 존재한다는 사실만으로 q가 따라 나옴을 보이는 것과 마찬가지입니다. 다음은 예시입니다

variable (α : Type) (p q : α → Prop) example (h : ∃ x, p x ∧ q x) : ∃ x, q x ∧ p x := Exists.elim h (fun w => fun hw : p w ∧ q w => show ∃ x, q x ∧ p x from ⟨w, hw.right, hw.left⟩)

존재 제거 규칙을 논리합 제거 규칙과 비교해 보면 도움이 될 수 있습니다. ∃ x : α, p x라는 주장은 a가 α의 모든 원소를 순회할 때의 명제 p a들을 모두 이은 커다란 논리합으로 생각할 수 있습니다. 익명 생성자 표기법 ⟨w, hw.right, hw.left⟩는 중첩된 생성자 적용을 축약한 것임에 유의하십시오. 이를 ⟨w, ⟨hw.right, hw.left⟩⟩와 같이 써도 똑같습니다.

존재 명제는 의존 타입 절에서 설명한 시그마 타입과 매우 비슷하다는 점에 주목하십시오. 차이점은 존재 명제는 명제이고, 시그마 타입은 타입이라는 것입니다. 그 밖의 점에서는 둘이 매우 비슷합니다. 술어 p : α → Prop과 타입족 β : α → Type이 주어졌을 때, h : p a와 h' : β a를 만족하는 항 a : α에 대해 항 Exists.intro a h는 타입 (∃ x : α, p x) : Prop을 가지고, Sigma.mk a h'는 타입 (Σ x : α, β x)를 가집니다. ∃와 Σ 사이의 유사성은 커리-하워드 동형의 또 다른 사례입니다.

Lean은 match 표현식을 사용하여 존재 한정자를 제거하는 더 편리한 방법을 제공합니다:

variable (α : Type) (p q : α → Prop) example (h : ∃ x, p x ∧ q x) : ∃ x, q x ∧ p x := match h with | ⟨w, hw⟩ => ⟨w, hw.right, hw.left⟩

match 표현식은 Lean의 함수 정의 시스템의 일부이며, 이 시스템은 복잡한 함수를 편리하고 표현력 있게 정의하는 방법을 제공합니다. 이번에도 커리-하워드 동형 덕분에 이 메커니즘을 증명 작성에도 활용할 수 있습니다. match 문은 존재 주장을 구성 요소 w와 hw로 "분해"하며, 이 구성 요소들은 문의 본문에서 명제를 증명하는 데 사용될 수 있습니다. 더 명확하게 하기 위해 match에서 사용되는 타입을 명시할 수 있습니다:

example (h : ∃ x, p x ∧ q x) : ∃ x, q x ∧ p x := match h with | ⟨(w : α), (hw : p w ∧ q w)⟩ => ⟨w, hw.right, hw.left⟩

심지어 match 문을 사용하여 논리곱을 동시에 분해할 수도 있습니다:

example (h : ∃ x, p x ∧ q x) : ∃ x, q x ∧ p x := match h with | ⟨w, hpw, hqw⟩ => ⟨w, hqw, hpw⟩

Lean은 패턴 매칭 let 표현식도 제공합니다:

example (h : ∃ x, p x ∧ q x) : ∃ x, q x ∧ p x := let ⟨w, hpw, hqw⟩ := h ⟨w, hqw, hpw⟩

이는 본질적으로 위의 match 구문에 대한 또 다른 표기법일 뿐입니다. Lean에서는 fun 표현식 안에서 암묵적인 match를 사용할 수도 있습니다:

example : (∃ x, p x ∧ q x) → ∃ x, q x ∧ p x := fun ⟨w, hpw, hqw⟩ => ⟨w, hqw, hpw⟩

귀납법과 재귀에서 이러한 변형들이 모두 더 일반적인 패턴 매칭 구문의 사례임을 살펴보겠습니다.

다음 예제에서는 IsEven a를 ∃ b, a = 2 * b로 정의한 다음, 두 짝수의 합이 짝수임을 보입니다.

def IsEven (a : Nat) := ∃ b, a = 2 * b theorem even_plus_even (h1 : IsEven a) (h2 : IsEven b) : IsEven (a + b) := Exists.elim h1 (fun w1 (hw1 : a = 2 * w1) => Exists.elim h2 (fun w2 (hw2 : b = 2 * w2) => Exists.intro (w1 + w2) (calc a + b _ = 2 * w1 + 2 * w2 := a:Natb:Nath1:IsEven ah2:IsEven bw1:Nathw1:a = 2 * w1w2:Nathw2:b = 2 * w2⊢ a + b = 2 * w1 + 2 * w2 All goals completed! 🐙 _ = 2 * (w1 + w2) := a:Natb:Nath1:IsEven ah2:IsEven bw1:Nathw1:a = 2 * w1w2:Nathw2:b = 2 * w2⊢ 2 * w1 + 2 * w2 = 2 * (w1 + w2) All goals completed! 🐙)))

이 장에서 설명한 여러 도구, 즉 match 문, 익명 생성자, rewrite 택틱을 사용하면 이 증명을 다음과 같이 간결하게 작성할 수 있습니다:

theorem even_plus_even (h1 : IsEven a) (h2 : IsEven b) : IsEven (a + b) := match h1, h2 with | ⟨w1, hw1⟩, ⟨w2, hw2⟩ => ⟨w1 + w2, a:Natb:Nath1:IsEven ah2:IsEven bw1:Nathw1:a = 2 * w1w2:Nathw2:b = 2 * w2⊢ a + b = 2 * (w1 + w2) All goals completed! 🐙⟩

구성적 “또는”이 고전적 “또는”보다 강한 것과 마찬가지로, 구성적 “존재한다” 역시 고전적 “존재한다”보다 강합니다. 예를 들어, 다음 함의에는 고전적 추론이 필요합니다. 구성적 관점에서 보면, 모든 x가 ¬ p를 만족하는 것은 아니라는 사실을 아는 것은 p를 만족하는 특정한 x를 가지고 있는 것과 같지 않기 때문입니다.

open Classical variable (p : α → Prop) example (h : ¬ ∀ x, ¬ p x) : ∃ x, p x := byContradiction (fun h1 : ¬ ∃ x, p x => have h2 : ∀ x, ¬ p x := fun x => fun h3 : p x => have h4 : ∃ x, p x := ⟨x, h3⟩ show False from h1 h4 show False from h h2)

다음은 존재 한정자와 관련된 몇 가지 일반적인 항등식입니다. 아래 연습 문제에서 가능한 한 많이 증명해 보시기를 권합니다. 또한 어느 것이 비구성적이어서 어떤 형태로든 고전적 추론이 필요한지 판단하는 일은 여러분에게 맡깁니다.

open Classical variable (α : Type) (p q : α → Prop) variable (r : Prop) declaration uses `sorry`example : (∃ x : α, r) → r := sorry declaration uses `sorry`example (a : α) : r → (∃ x : α, r) := sorry declaration uses `sorry`example : (∃ x, p x ∧ r) ↔ (∃ x, p x) ∧ r := sorry declaration uses `sorry`example : (∃ x, p x ∨ q x) ↔ (∃ x, p x) ∨ (∃ x, q x) := sorry declaration uses `sorry`example : (∀ x, p x) ↔ ¬ (∃ x, ¬ p x) := sorry declaration uses `sorry`example : (∃ x, p x) ↔ ¬ (∀ x, ¬ p x) := sorry declaration uses `sorry`example : (¬ ∃ x, p x) ↔ (∀ x, ¬ p x) := sorry declaration uses `sorry`example : (¬ ∀ x, p x) ↔ (∃ x, ¬ p x) := sorry declaration uses `sorry`example : (∀ x, p x → r) ↔ (∃ x, p x) → r := sorry declaration uses `sorry`example (a : α) : (∃ x, p x → r) ↔ (∀ x, p x) → r := sorry declaration uses `sorry`example (a : α) : (∃ x, r → p x) ↔ (r → ∃ x, p x) := sorry

두 번째 예제와 마지막 두 예제에는 α 타입의 원소 a가 적어도 하나 존재한다는 가정이 필요하다는 점에 유의하십시오.

다음은 그중 더 어려운 문제 두 개의 풀이입니다:

open Classical variable (α : Type) (p q : α → Prop) variable (a : α) variable (r : Prop) example : (∃ x, p x ∨ q x) ↔ (∃ x, p x) ∨ (∃ x, q x) := Iff.intro (fun ⟨a, (h1 : p a ∨ q a)⟩ => Or.elim h1 (fun hpa : p a => Or.inl ⟨a, hpa⟩) (fun hqa : q a => Or.inr ⟨a, hqa⟩)) (fun h : (∃ x, p x) ∨ (∃ x, q x) => Or.elim h (fun ⟨a, hpa⟩ => ⟨a, (Or.inl hpa)⟩) (fun ⟨a, hqa⟩ => ⟨a, (Or.inr hqa)⟩)) example : (∃ x, p x → r) ↔ (∀ x, p x) → r := Iff.intro (fun ⟨b, (hb : p b → r)⟩ => fun h2 : ∀ x, p x => show r from hb (h2 b)) (fun h1 : (∀ x, p x) → r => show ∃ x, p x → r from byCases (fun hap : ∀ x, p x => ⟨a, λ Variable name `h'` is not explicitly referenced. Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning: [apply] _h' Note: This linter can be disabled with `set_option linter.unusedVariables false`h' => h1 hap⟩) (fun hnap : ¬ ∀ x, p x => byContradiction (fun hnex : ¬ ∃ x, p x → r => have hap : ∀ x, p x := fun x => byContradiction (fun hnp : ¬ p x => have hex : ∃ x, p x → r := ⟨x, (fun hp => absurd hp hnp)⟩ show False from hnex hex) show False from hnap hap)))

4.5. 증명 언어 더 알아보기🔗

fun, have, show와 같은 키워드를 사용하면 비형식적인 수학 증명의 구조를 그대로 반영하는 형식적 증명 항을 작성할 수 있음을 살펴보았습니다. 이 절에서는 증명 언어에서 흔히 유용하게 쓰이는 몇 가지 추가 기능을 다룹니다.

먼저, 익명 have 표현식을 사용하면 이름을 붙이지 않고도 보조 목표를 도입할 수 있습니다. 이렇게 도입된 마지막 표현식은 this 키워드로 참조할 수 있습니다:

variable (f : Nat → Nat) variable (h : ∀ x : Nat, f x ≤ f (x + 1)) example : f 0 ≤ f 3 := have : f 0 ≤ f 1 := h 0 have : f 0 ≤ f 2 := Nat.le_trans this (h 1) show f 0 ≤ f 3 from Nat.le_trans this (h 2)

증명은 흔히 한 사실에서 다음 사실로 나아가므로, 이 방법은 수많은 레이블 때문에 생기는 번잡함을 없애는 데 효과적일 수 있습니다.

목표를 추론할 수 있을 때는 by assumption이라고 써서 Lean이 대신 증명을 채워 넣도록 요청할 수도 있습니다:

example : f 0 ≤ f 3 := have : f 0 ≤ f 1 := h 0 have : f 0 ≤ f 2 := Nat.le_trans (f:Nat → Nath:∀ (x : Nat), f x ≤ f (x + 1)this:f 0 ≤ f 1⊢ f 0 ≤ f 1 All goals completed! 🐙) (h 1) show f 0 ≤ f 3 from Nat.le_trans (f:Nat → Nath:∀ (x : Nat), f x ≤ f (x + 1)this✝:f 0 ≤ f 1this:f 0 ≤ f 2⊢ f 0 ≤ f 2 All goals completed! 🐙) (h 2)

이는 Lean에게 assumption 택틱을 사용하라고 지시하며, 이 택틱은 다시 로컬 컨텍스트에서 적절한 가설을 찾아 목표를 증명합니다. assumption 택틱에 대해서는 다음 장에서 더 자세히 배우겠습니다.

또한 ‹p›라고 작성하여 Lean에게 증명을 채워 달라고 요청할 수도 있습니다. 여기서 p는 Lean이 맥락에서 증명을 찾아 주기를 바라는 명제입니다. 이 꺾쇠 따옴표는 각각 \f<와 \f>로 입력할 수 있습니다. 문자 “f”는 “French(프랑스어)”를 뜻하는데, 이 유니코드 기호들이 프랑스어 인용 부호로도 쓰일 수 있기 때문입니다. 실제로 이 표기법은 Lean에서 다음과 같이 정의되어 있습니다:

notation "‹" p "›" => show p by assumption

이 방식은 추론해야 하는 가정의 타입을 명시적으로 제시하므로 by assumption을 사용하는 것보다 더 견고합니다. 또한 증명을 더 읽기 쉽게 만들어 줍니다. 다음은 좀 더 정교한 예제입니다:

variable (f : Nat → Nat) variable (h : ∀ x : Nat, f x ≤ f (x + 1)) example : f 0 ≥ f 1 → f 1 ≥ f 2 → f 0 = f 2 := fun _ : f 0 ≥ f 1 => fun _ : f 1 ≥ f 2 => have : f 0 ≥ f 2 := Nat.le_trans ‹f 1 ≥ f 2› ‹f 0 ≥ f 1› have : f 0 ≤ f 2 := Nat.le_trans (h 0) (h 1) show f 0 = f 2 from Nat.le_antisymm this ‹f 0 ≥ f 2›

이런 식으로 프랑스식 따옴표를 사용하면 익명으로 도입된 것뿐만 아니라 맥락에 있는 어떤 것이든 참조할 수 있다는 점을 기억해 두십시오. 또한 이 표기는 명제에만 쓸 수 있는 것도 아닙니다. 다만 데이터에 사용하는 것은 다소 어색합니다:

example (n : Nat) : Nat := ‹Nat›

이후에 Lean 매크로 시스템을 사용하여 증명 언어를 확장하는 방법을 보여 드리겠습니다.

4.6. 연습 문제🔗

  1. 다음 동치 명제들을 증명하십시오:

    variable (α : Type) (p q : α → Prop) declaration uses `sorry`example : (∀ x, p x ∧ q x) ↔ (∀ x, p x) ∧ (∀ x, q x) := sorry declaration uses `sorry`example : (∀ x, p x → q x) → (∀ x, p x) → (∀ x, q x) := sorry declaration uses `sorry`example : (∀ x, p x) ∨ (∀ x, q x) → ∀ x, p x ∨ q x := sorry

    마지막 예제에서 역방향 함의를 왜 유도할 수 없는지도 이해해 보십시오.

  2. 수식의 한 구성 요소가 한정된 변수에 의존하지 않을 때, 그 구성 요소를 전칭 한정자 바깥으로 꺼낼 수 있는 경우가 많습니다. 다음을 증명해 보십시오(이 중 두 번째 것의 한쪽 방향은 고전 논리를 필요로 합니다):

    variable (α : Type) (p q : α → Prop) variable (r : Prop) declaration uses `sorry`example : α → ((∀ x : α, r) ↔ r) := sorry declaration uses `sorry`example : (∀ x, p x ∨ r) ↔ (∀ x, p x) ∨ r := sorry declaration uses `sorry`example : (∀ x, r → p x) ↔ (r → ∀ x, p x) := sorry
  3. “이발사 역설”, 즉 어떤 마을에 자기 자신을 면도하지 않는 모든 남자를, 그리고 오직 그런 남자만을 면도해 주는 (남성) 이발사가 있다는 주장을 생각해 보십시오. 이것이 모순임을 증명하십시오:

    variable (men : Type) (barber : men) variable (shaves : men → men → Prop) declaration uses `sorry`example (h : ∀ x : men, shaves barber x ↔ ¬ shaves x x) : False := sorry
  4. 매개변수가 없다면 Prop 타입의 표현식은 단지 하나의 주장일 뿐이라는 점을 기억하십시오. 아래에 있는 prime과 Fermat_prime의 정의를 채워 넣고, 주어진 각 주장을 구성하십시오. 예를 들어, 모든 자연수 n에 대해 n보다 큰 소수가 존재한다고 주장함으로써 소수가 무한히 많다는 것을 표현할 수 있습니다. 골드바흐의 약한 추측은 5보다 큰 모든 홀수가 세 소수의 합이라고 주장합니다. 필요하다면 페르마 소수의 정의나 다른 명제들을 찾아보십시오.

    def declaration uses `sorry`even (n : Nat) : Prop := sorry def declaration uses `sorry`prime (n : Nat) : Prop := sorry def declaration uses `sorry`infinitely_many_primes : Prop := sorry def declaration uses `sorry`Fermat_prime (n : Nat) : Prop := sorry def declaration uses `sorry`infinitely_many_Fermat_primes : Prop := sorry def declaration uses `sorry`goldbach_conjecture : Prop := sorry def declaration uses `sorry`Goldbach's_weak_conjecture : Prop := sorry def declaration uses `sorry`Fermat's_last_theorem : Prop := sorry
  5. 존재 한정자 절에 나열된 항등식을 가능한 한 많이 증명하십시오.