Lean 4로 정리 증명하기

5. 택틱🔗

이 장에서는 택틱을 사용하여 증명을 구성하는 또 다른 방법을 설명합니다. 증명 항은 수학적 증명을 표현한 것이며, 택틱은 그러한 증명을 구성하는 방법을 기술하는 명령 또는 지시입니다. 비형식적으로는 “정방향을 증명하려면 정의를 펼치고, 앞의 보조정리를 적용한 다음, 단순화하십시오.”라고 말하면서 수학적 증명을 시작할 수 있습니다. 이러한 말이 독자에게 해당 증명을 찾는 방법을 알려 주는 지시인 것처럼, 택틱은 Lean에게 증명 항을 구성하는 방법을 알려 주는 지시입니다. 택틱은 증명을 분해하여 목표를 한 번에 한 단계씩 처리하는 점진적인 증명 작성 방식을 자연스럽게 지원합니다.

택틱의 나열로 이루어진 증명을 “택틱 스타일” 증명이라고 부르겠습니다. 이는 지금까지 살펴본 증명 항 작성 방식과 대비되는 것으로, 후자는 “항 스타일” 증명이라고 부르겠습니다. 각 스타일에는 저마다 장단점이 있습니다. 예를 들어, 택틱 스타일 증명은 읽는 사람이 각 명령의 결과를 예측하거나 추측해야 하므로 읽기가 더 어려울 수 있습니다. 하지만 더 짧고 작성하기도 더 쉬울 수 있습니다. 게다가 자동화된 절차 자체가 택틱이므로, 택틱은 Lean의 자동화 기능을 사용하는 관문이 됩니다.

5.1. 택틱 모드 진입하기🔗

개념적으로, 정리를 진술하거나 have 문을 도입하면 목표가 생성됩니다. 즉, 기대 타입을 갖는 항을 구성해야 한다는 목표입니다. 예를 들어, 다음은 상수 p q : Prop, hp : p, hq : q가 있는 문맥에서 p ∧ q ∧ p 타입의 항을 구성하는 목표를 생성합니다:

theorem declaration uses `sorry`test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := p:Propq:Prophp:phq:q⊢ p ∧ q ∧ p All goals completed! 🐙

이 목표는 다음과 같이 작성할 수 있습니다:

p:Propq:Prophp:phq:q⊢ p ∧ q ∧ p

실제로 위 예제에서 “sorry”를 밑줄로 바꾸면, Lean은 해결되지 않은 채 남아 있는 것이 바로 이 목표라고 보고합니다.

보통은 명시적인 항을 작성하여 이러한 목표를 달성합니다. 그러나 Lean에서는 항이 필요한 곳이라면 어디에서든 그 대신 by <tactics> 블록을 넣을 수 있습니다. 여기서 <tactics>는 세미콜론이나 줄 바꿈으로 구분된 일련의 명령입니다. 위의 정리는 다음과 같은 방식으로 증명할 수 있습니다.

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := p:Propq:Prophp:phq:q⊢ p ∧ q ∧ p p:Propq:Prophp:phq:q⊢ pp:Propq:Prophp:phq:q⊢ q ∧ p p:Propq:Prophp:phq:q⊢ q ∧ p p:Propq:Prophp:phq:q⊢ qp:Propq:Prophp:phq:q⊢ p p:Propq:Prophp:phq:q⊢ p All goals completed! 🐙

흔히 by 키워드를 앞 줄에 두어, 위의 예제를 다음과 같이 작성합니다.

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := p:Propq:Prophp:phq:q⊢ p ∧ q ∧ p p:Propq:Prophp:phq:q⊢ pp:Propq:Prophp:phq:q⊢ q ∧ p p:Propq:Prophp:phq:q⊢ q ∧ p p:Propq:Prophp:phq:q⊢ qp:Propq:Prophp:phq:q⊢ p p:Propq:Prophp:phq:q⊢ p All goals completed! 🐙

apply 택틱은 표현식을 0개 이상의 인자를 받는 함수로 간주하여 적용합니다. 이 택틱은 표현식의 결론을 현재 목표와 단일화하고, 남은 인자 중 뒤따르는 인자가 의존하지 않는 인자마다 새 목표를 만듭니다. 위 예제에서 apply And.intro 명령은 두 개의 하위 목표를 만듭니다:

p:Propq:Prophp:phq:q⊢ pp:Propq:Prophp:phq:q⊢ q ∧ p

첫 번째 목표는 exact hp 명령으로 달성됩니다. exact 명령은 apply의 변형일 뿐이며, 주어진 표현식이 목표를 정확히 채워야 함을 나타냅니다. 택틱 증명에서는 이 명령을 사용하는 것이 좋은 습관입니다. 이 명령이 실패하면 무언가 잘못되었다는 신호이기 때문입니다. 또한 이 명령은 apply보다 더 견고합니다. 정교화기가 적용되는 표현식을 처리할 때 목표의 대상이 제공하는 기대 타입을 고려하기 때문입니다. 하지만 이 경우에는 apply를 사용해도 똑같이 잘 작동합니다.

결과로 만들어진 증명 항은 #print 명령으로 확인할 수 있습니다:

theorem test : ∀ (p q : Prop), p → q → p ∧ q ∧ p := fun p q hp hq => ⟨hp, ⟨hq, hp⟩⟩#print test
theorem test : ∀ (p q : Prop), p → q → p ∧ q ∧ p :=
fun p q hp hq => ⟨hp, ⟨hq, hp⟩⟩

택틱 스크립트는 점진적으로 작성할 수 있습니다. VS Code에서는 CtrlShiftEnter를 눌러 메시지를 표시하는 창을 열 수 있으며, 이후 커서가 택틱 블록 안에 있을 때마다 그 창에 현재 목표가 표시됩니다. 증명이 완료되지 않은 경우 by 토큰에 빨간색 물결 밑줄이 표시되며, 오류 메시지에는 남은 목표가 포함됩니다.

택틱 명령은 단일 식별자뿐만 아니라 복합 표현식도 인자로 받을 수 있습니다. 다음은 앞의 증명을 더 짧게 작성한 것입니다:

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := p:Propq:Prophp:phq:q⊢ p ∧ q ∧ p p:Propq:Prophp:phq:q⊢ q ∧ p All goals completed! 🐙

예상대로, 이것은 정확히 같은 증명 항을 생성합니다:

theorem test : ∀ (p q : Prop), p → q → p ∧ q ∧ p := fun p q hp hq => ⟨hp, ⟨hq, hp⟩⟩#print test
theorem test : ∀ (p q : Prop), p → q → p ∧ q ∧ p :=
fun p q hp hq => ⟨hp, ⟨hq, hp⟩⟩

여러 택틱 적용은 세미콜론으로 이어 붙여 한 줄에 작성할 수 있습니다.

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := p:Propq:Prophp:phq:q⊢ p ∧ q ∧ p p:Propq:Prophp:phq:q⊢ q ∧ p; All goals completed! 🐙

여러 하위 목표를 만들어 낼 수 있는 택틱은 흔히 그 하위 목표들에 태그를 붙입니다. 예를 들어, apply And.intro 택틱은 첫 번째 하위 목표에 leftp:Propq:Prophp:phq:q⊢ p라는 태그를, 두 번째 하위 목표에 rightp:Propq:Prophp:phq:q⊢ q ∧ p라는 태그를 붙였습니다. apply 택틱의 경우, 태그는 And.intro 선언에 사용된 매개변수 이름으로부터 추론됩니다. case <tag> => <tactics> 표기법을 사용하여 택틱을 구조화할 수 있습니다. 다음은 이 장에서 다룬 첫 번째 택틱 증명을 구조화한 버전입니다.

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := p:Propq:Prophp:phq:q⊢ p ∧ q ∧ p p:Propq:Prophp:phq:q⊢ pp:Propq:Prophp:phq:q⊢ q ∧ p case left p:Propq:Prophp:phq:q⊢ p All goals completed! 🐙 case right p:Propq:Prophp:phq:q⊢ q ∧ p p:Propq:Prophp:phq:q⊢ qp:Propq:Prophp:phq:q⊢ p case left p:Propq:Prophp:phq:q⊢ q All goals completed! 🐙 case right p:Propq:Prophp:phq:q⊢ p All goals completed! 🐙

case 표기법을 사용하면 leftp:Propq:Prophp:phq:q⊢ p보다 rightp:Propq:Prophp:phq:q⊢ q ∧ p 하위 목표를 먼저 해결할 수 있습니다:

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := p:Propq:Prophp:phq:q⊢ p ∧ q ∧ p p:Propq:Prophp:phq:q⊢ pp:Propq:Prophp:phq:q⊢ q ∧ p case right p:Propq:Prophp:phq:q⊢ q ∧ p p:Propq:Prophp:phq:q⊢ qp:Propq:Prophp:phq:q⊢ p case left p:Propq:Prophp:phq:q⊢ q All goals completed! 🐙 case right p:Propq:Prophp:phq:q⊢ p All goals completed! 🐙 case left p:Propq:Prophp:phq:q⊢ p All goals completed! 🐙

Lean은 case 블록 안에서 다른 목표들을 숨긴다는 점에 유의하십시오. case left => 이후의 증명 상태는 다음과 같습니다:

p:Propq:Prophp:phq:q⊢ q

이를 두고 case가 선택된 목표에 “집중”한다고 말합니다. 또한 case 블록이 끝날 때 선택된 목표가 완전히 해결되지 않았으면 Lean은 오류를 표시합니다.

단순한 하위 목표라면 태그로 하위 목표를 선택할 필요까지는 없겠지만, 그래도 증명을 구조화하고 싶을 수 있습니다. Lean은 증명을 구조화할 수 있도록 “불릿” 표기법 . <tactics>(또는 · <tactics>)도 제공합니다.

theorem test (p q : Prop) (hp : p) (hq : q) : p ∧ q ∧ p := p:Propq:Prophp:phq:q⊢ p ∧ q ∧ p p:Propq:Prophp:phq:q⊢ pp:Propq:Prophp:phq:q⊢ q ∧ p p:Propq:Prophp:phq:q⊢ p All goals completed! 🐙 p:Propq:Prophp:phq:q⊢ q ∧ p p:Propq:Prophp:phq:q⊢ qp:Propq:Prophp:phq:q⊢ p p:Propq:Prophp:phq:q⊢ q All goals completed! 🐙 p:Propq:Prophp:phq:q⊢ p All goals completed! 🐙

5.2. 기본 택틱🔗

apply와 exact 외에 유용한 택틱으로 가설을 도입하는 intro도 있습니다. 다음은 이전 장에서 증명했던 명제 논리의 항등식을 이번에는 택틱을 사용하여 증명한 예입니다.

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) ↔ p ∧ q ∨ p ∧ r p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ rp:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)⊢ q → p ∧ q ∨ p ∧ rp:Propq:Propr:Proph:p ∧ (q ∨ r)⊢ r → p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)⊢ q → p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)hq:q⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)hq:q⊢ p ∧ q p:Propq:Propr:Proph:p ∧ (q ∨ r)hq:q⊢ pp:Propq:Propr:Proph:p ∧ (q ∨ r)hq:q⊢ q p:Propq:Propr:Proph:p ∧ (q ∨ r)hq:q⊢ p All goals completed! 🐙 p:Propq:Propr:Proph:p ∧ (q ∨ r)hq:q⊢ q All goals completed! 🐙 p:Propq:Propr:Proph:p ∧ (q ∨ r)⊢ r → p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)hr:r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)hr:r⊢ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)hr:r⊢ pp:Propq:Propr:Proph:p ∧ (q ∨ r)hr:r⊢ r p:Propq:Propr:Proph:p ∧ (q ∨ r)hr:r⊢ p All goals completed! 🐙 p:Propq:Propr:Proph:p ∧ (q ∨ r)hr:r⊢ r All goals completed! 🐙 p:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ r⊢ p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ r⊢ p ∧ q → p ∧ (q ∨ r)p:Propq:Propr:Proph:p ∧ q ∨ p ∧ r⊢ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ r⊢ p ∧ q → p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhpq:p ∧ q⊢ p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhpq:p ∧ q⊢ pp:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhpq:p ∧ q⊢ q ∨ r p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhpq:p ∧ q⊢ p All goals completed! 🐙 p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhpq:p ∧ q⊢ q ∨ r p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhpq:p ∧ q⊢ q All goals completed! 🐙 p:Propq:Propr:Proph:p ∧ q ∨ p ∧ r⊢ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhpr:p ∧ r⊢ p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhpr:p ∧ r⊢ pp:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhpr:p ∧ r⊢ q ∨ r p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhpr:p ∧ r⊢ p All goals completed! 🐙 p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhpr:p ∧ r⊢ q ∨ r p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhpr:p ∧ r⊢ r All goals completed! 🐙

intro 명령은 더 일반적으로 임의의 타입의 변수를 도입하는 데에도 사용할 수 있습니다:

example (α : Type) : α → α := α:Type⊢ α → α α:Typea:α⊢ α All goals completed! 🐙 example (α : Type) : ∀ x : α, x = x := α:Type⊢ ∀ (x : α), x = x α:Typex:α⊢ x = x All goals completed! 🐙

이를 사용하여 여러 변수를 도입할 수 있습니다:

example : ∀ a b c : Nat, a = b → a = c → c = b := ⊢ ∀ (a b c : Nat), a = b → a = c → c = b a:Natb:Natc:Nath₁:a = bh₂:a = c⊢ c = b All goals completed! 🐙

apply 택틱이 함수 적용을 대화식으로 구성하는 명령인 것처럼, intro 택틱은 함수 추상화(즉, fun x => e 형태의 항)를 대화식으로 구성하는 명령입니다. 람다 추상화 표기법에서와 마찬가지로, intro 택틱에서도 암묵적인 match를 사용할 수 있습니다.

example (p q : α → Prop) : (∃ x, p x ∧ q x) → ∃ x, q x ∧ p x := α:Sort u_1p:α → Propq:α → Prop⊢ (∃ x, p x ∧ q x) → ∃ x, q x ∧ p x α:Sort u_1p:α → Propq:α → Propw:αhpw:p whqw:q w⊢ ∃ x, q x ∧ p x All goals completed! 🐙

match 표현식에서처럼 여러 대안을 제시할 수도 있습니다.

example (p q : α → Prop) : (∃ x, p x ∨ q x) → ∃ x, q x ∨ p x := α:Sort u_1p:α → Propq:α → Prop⊢ (∃ x, p x ∨ q x) → ∃ x, q x ∨ p x intro α:Sort u_1p:α → Propq:α → Propx✝:∃ x, p x ∨ q xw:αh:p w⊢ ∃ x, q x ∨ p x All goals completed! 🐙 α:Sort u_1p:α → Propq:α → Propx✝:∃ x, p x ∨ q xw:αh:q w⊢ ∃ x, q x ∨ p x All goals completed! 🐙

intros 택틱은 인수 없이도 사용할 수 있으며, 이 경우 이름을 스스로 정하고 가능한 한 많은 변수를 도입합니다. 잠시 후 이에 대한 예제를 보게 될 것입니다.

assumption 택틱은 현재 목표의 컨텍스트에 있는 가정들을 살펴보고, 결론과 일치하는 가정이 있으면 그 가정을 적용합니다.

variable (x y z w : Nat) example (h₁ : x = y) (h₂ : y = z) (h₃ : z = w) : x = w := x:Naty:Natz:Natw:Nath₁:x = yh₂:y = zh₃:z = w⊢ x = w x:Naty:Natz:Natw:Nath₁:x = yh₂:y = zh₃:z = w⊢ y = w x:Naty:Natz:Natw:Nath₁:x = yh₂:y = zh₃:z = w⊢ z = w All goals completed! 🐙 -- applied h₃

필요하면 결론에 있는 메타변수를 단일화합니다:

variable (x y z w : Nat) example (h₁ : x = y) (h₂ : y = z) (h₃ : z = w) : x = w := x:Naty:Natz:Natw:Nath₁:x = yh₂:y = zh₃:z = w⊢ x = w x:Naty:Natz:Natw:Nath₁:x = yh₂:y = zh₃:z = w⊢ x = ?bx:Naty:Natz:Natw:Nath₁:x = yh₂:y = zh₃:z = w⊢ ?b = wx:Naty:Natz:Natw:Nath₁:x = yh₂:y = zh₃:z = w⊢ Nat x:Naty:Natz:Natw:Nath₁:x = yh₂:y = zh₃:z = w⊢ y = w -- solves x = ?b with h₁ x:Naty:Natz:Natw:Nath₁:x = yh₂:y = zh₃:z = w⊢ y = ?h₂.bx:Naty:Natz:Natw:Nath₁:x = yh₂:y = zh₃:z = w⊢ ?h₂.b = wx:Naty:Natz:Natw:Nath₁:x = yh₂:y = zh₃:z = w⊢ Nat x:Naty:Natz:Natw:Nath₁:x = yh₂:y = zh₃:z = w⊢ z = w -- solves y = ?h₂.b with h₂ All goals completed! 🐙 -- solves z = w with h₃

다음 예제는 intros 명령을 사용하여 세 변수와 두 가설을 자동으로 도입합니다:

example : ∀ a b c : Nat, a = b → a = c → c = b := ⊢ ∀ (a b c : Nat), a = b → a = c → c = b a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ c✝ = b✝ a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ c✝ = ?ba✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ ?b = b✝a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ Nat a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ ?b✝ = c✝a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ ?b = b✝a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ Nat a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ a✝² = b✝ All goals completed! 🐙

Lean이 자동으로 생성한 이름은 기본적으로 접근할 수 없다는 점에 유의하십시오. 이는 택틱 증명이 자동으로 생성된 이름에 의존하지 않도록 하여, 결과적으로 증명을 더 견고하게 만들기 위함입니다. 하지만 unhygienic 조합자를 사용하면 이 제한을 해제할 수 있습니다.

example : ∀ a b c : Nat, a = b → a = c → c = b := ⊢ ∀ (a b c : Nat), a = b → a = c → c = b unhygienic a:Natb:Natc:Nata_1:a = ba_2:a = c⊢ c = b a:Natb:Natc:Nata_1:a = ba_2:a = c⊢ c = ?ba:Natb:Natc:Nata_1:a = ba_2:a = c⊢ ?b = ba:Natb:Natc:Nata_1:a = ba_2:a = c⊢ Nat a:Natb:Natc:Nata_1:a = ba_2:a = c⊢ ?b✝ = ca:Natb:Natc:Nata_1:a = ba_2:a = c⊢ ?b = ba:Natb:Natc:Nata_1:a = ba_2:a = c⊢ Nat a:Natb:Natc:Nata_1:a = ba_2:a = c⊢ a = b All goals completed! 🐙

rename_i 택틱을 사용해 컨텍스트에서 가장 최근의 접근할 수 없는 이름들의 이름을 바꿀 수도 있습니다. 다음 예제에서 rename_i h1 _ h2 택틱은 컨텍스트의 마지막 세 가설 중 두 개의 이름을 바꿉니다.

example : ∀ a b c d : Nat, a = b → a = d → a = c → c = b := ⊢ ∀ (a b c d : Nat), a = b → a = d → a = c → c = b a✝³:Natb✝:Natc✝:Natd✝:Nata✝²:a✝³ = b✝a✝¹:a✝³ = d✝a✝:a✝³ = c✝⊢ c✝ = b✝ a✝¹:Natb✝:Natc✝:Natd✝:Nath1:a✝³ = b✝a✝:a✝³ = d✝h2:a✝³ = c✝⊢ c✝ = b✝ a✝¹:Natb✝:Natc✝:Natd✝:Nath1:a✝³ = b✝a✝:a✝³ = d✝h2:a✝³ = c✝⊢ c✝ = ?ba✝¹:Natb✝:Natc✝:Natd✝:Nath1:a✝³ = b✝a✝:a✝³ = d✝h2:a✝³ = c✝⊢ ?b = b✝a✝¹:Natb✝:Natc✝:Natd✝:Nath1:a✝³ = b✝a✝:a✝³ = d✝h2:a✝³ = c✝⊢ Nat a✝¹:Natb✝:Natc✝:Natd✝:Nath1:a✝³ = b✝a✝:a✝³ = d✝h2:a✝³ = c✝⊢ ?b✝ = c✝a✝¹:Natb✝:Natc✝:Natd✝:Nath1:a✝³ = b✝a✝:a✝³ = d✝h2:a✝³ = c✝⊢ ?b = b✝a✝¹:Natb✝:Natc✝:Natd✝:Nath1:a✝³ = b✝a✝:a✝³ = d✝h2:a✝³ = c✝⊢ Nat a✝¹:Natb✝:Natc✝:Natd✝:Nath1:a✝³ = b✝a✝:a✝³ = d✝h2:a✝³ = c✝⊢ a✝³ = b✝ All goals completed! 🐙

rfl 택틱은 정의적으로 같은 인자에 반사 관계를 적용한 형태의 목표를 해결합니다. 상등은 반사적입니다:

example (y : Nat) : (fun Variable name `x` 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] _x Note: This linter can be disabled with `set_option linter.unusedVariables false`x : Nat => 0) y = 0 := y:Nat⊢ (fun x => 0) y = 0 All goals completed! 🐙

repeat 조합자를 사용하면 택틱을 여러 번 적용할 수 있습니다:

example : ∀ a b c : Nat, a = b → a = c → c = b := ⊢ ∀ (a b c : Nat), a = b → a = c → c = b a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ c✝ = b✝ a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ c✝ = ?ba✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ ?b = b✝a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ Nat a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ ?b✝ = c✝a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ ?b = b✝a✝²:Natb✝:Natc✝:Nata✝¹:a✝² = b✝a✝:a✝² = c✝⊢ Nat repeat All goals completed! 🐙

때때로 유용한 또 다른 택틱으로 revert 택틱이 있습니다. 이 택틱은 어떤 의미에서 intro의 역에 해당합니다:

example (x : Nat) : x = x := x:Nat⊢ x = x ⊢ ∀ (x : Nat), x = x y:Nat⊢ y = y All goals completed! 🐙

revert x 이후의 증명 상태는 다음과 같습니다:

⊢ ∀ (x : Nat), x = x

intro y 이후에는 다음과 같습니다:

y:Nat⊢ y = y

가설을 목표로 옮기면 함의가 됩니다:

example (x y : Nat) (h : x = y) : y = x := x:Naty:Nath:x = y⊢ y = x x:Naty:Nat⊢ x = y → y = x x:Naty:Nath₁:x = y⊢ y = x -- goal is x y : Nat, h₁ : x = y ⊢ y = x x:Naty:Nath₁:x = y⊢ x = y All goals completed! 🐙

revert h를 실행하면 증명 상태는 다음과 같습니다.

x:Naty:Nat⊢ x = y → y = x

intro h₁ 이후에는 다음과 같습니다:

x:Naty:Nath₁:x = y⊢ y = x

하지만 revert는 한층 더 영리합니다. 맥락의 한 요소만 되돌리는 것이 아니라, 그 요소에 의존하는 맥락의 이후 요소들까지 모두 되돌리기 때문입니다. 예를 들어, 위 예제에서 x를 되돌리면 h도 함께 되돌려집니다:

example (x y : Nat) (h : x = y) : y = x := x:Naty:Nath:x = y⊢ y = x y:Nat⊢ ∀ (x : Nat), x = y → y = x y:Natx✝:Nath✝:x✝ = y⊢ y = x✝ y:Natx✝:Nath✝:x✝ = y⊢ x✝ = y All goals completed! 🐙

revert x를 실행하면 목표는 다음과 같습니다.

y:Nat⊢ ∀ (x : Nat), x = y → y = x

컨텍스트의 여러 요소를 한 번에 되돌릴 수도 있습니다:

example (x y : Nat) (h : x = y) : y = x := x:Naty:Nath:x = y⊢ y = x ⊢ ∀ (x y : Nat), x = y → y = x x✝:Naty✝:Nath✝:x✝ = y✝⊢ y✝ = x✝ x✝:Naty✝:Nath✝:x✝ = y✝⊢ x✝ = y✝ All goals completed! 🐙

revert x y를 실행하면 목표는 다음과 같습니다:

⊢ ∀ (x y : Nat), x = y → y = x

revert는 로컬 컨텍스트의 요소, 즉 로컬 변수나 가설에만 적용할 수 있습니다. 하지만 generalize 택틱을 사용하면 목표에 있는 임의의 표현식을 새로운 변수로 대체할 수 있습니다:

example : 3 = 3 := ⊢ 3 = 3 x:Nat⊢ x = x ⊢ ∀ (x : Nat), x = x y:Nat⊢ y = y All goals completed! 🐙

특히 generalize를 적용한 후의 목표는 다음과 같습니다.

x:Nat⊢ x = x

위 표기법은 3을 임의의 변수 x로 두어 목표를 일반화한다는 뜻으로 기억하면 됩니다. 주의하십시오. 모든 일반화가 목표의 타당성을 보존하는 것은 아닙니다. 여기서 generalize는 rfl을 사용하여 증명할 수 있었던 목표를 증명할 수 없는 목표로 바꿉니다:

declaration uses `sorry`example : 2 + 3 = 5 := ⊢ 2 + 3 = 5 x:Nat⊢ 2 + x = 5 All goals completed! 🐙

이 예제에서 sorry 택틱은 sorry 증명 항에 대응하는 택틱입니다. 이 택틱은 현재 목표를 닫으며, sorry가 사용되었다는 통상적인 경고를 발생시킵니다. 이전 목표의 타당성을 유지하기 위해, generalize 택틱은 3이 x로 대체되었다는 사실을 레코드로 남길 수 있게 해 줍니다. 레이블 하나만 제공하면 되며, generalize는 그 레이블을 사용하여 로컬 컨텍스트에 대입을 저장합니다:

example : 2 + 3 = 5 := ⊢ 2 + 3 = 5 x:Nath:3 = x⊢ 2 + x = 5 All goals completed! 🐙

generalize h : 3 = x를 실행하고 나면 h는 3 = x의 증명입니다:

x:Nath:3 = x⊢ 2 + x = 5

여기서 재작성 택틱 rw는 h를 사용하여 x를 다시 3으로 바꿉니다. rw 택틱은 아래에서 다룹니다.

5.3. 더 많은 택틱🔗

명제와 데이터를 구성하거나 분해하는 데 유용한 택틱이 몇 가지 더 있습니다. 예를 들어, p ∨ q 형태의 목표에는 apply Or.inl이나 apply Or.inr과 같은 택틱을 사용합니다. 반대로, cases 택틱을 사용하여 논리합을 분해할 수 있습니다:

example (p q : Prop) : p ∨ q → q ∨ p := p:Propq:Prop⊢ p ∨ q → q ∨ p p:Propq:Proph:p ∨ q⊢ q ∨ p cases h with p:Propq:Prophp:p⊢ q ∨ p p:Propq:Prophp:p⊢ p; All goals completed! 🐙 p:Propq:Prophq:q⊢ q ∨ p p:Propq:Prophq:q⊢ q; All goals completed! 🐙

이 구문은 match 표현식에서 사용하는 구문과 비슷하다는 점에 유의하십시오. 새로 생긴 하위 목표는 어떤 순서로든 해결할 수 있습니다:

example (p q : Prop) : p ∨ q → q ∨ p := p:Propq:Prop⊢ p ∨ q → q ∨ p p:Propq:Proph:p ∨ q⊢ q ∨ p cases h with p:Propq:Prophq:q⊢ q ∨ p p:Propq:Prophq:q⊢ q; All goals completed! 🐙 p:Propq:Prophp:p⊢ q ∨ p p:Propq:Prophp:p⊢ p; All goals completed! 🐙

with 없이 (구조화되지 않은) cases를 사용하고 각 경우마다 택틱을 하나씩 사용할 수도 있습니다:

example (p q : Prop) : p ∨ q → q ∨ p := p:Propq:Prop⊢ p ∨ q → q ∨ p p:Propq:Proph:p ∨ q⊢ q ∨ p p:Propq:Proph✝:p⊢ q ∨ pp:Propq:Proph✝:q⊢ q ∨ p p:Propq:Proph✝:p⊢ pp:Propq:Proph✝:q⊢ q ∨ p p:Propq:Proph✝:q⊢ q ∨ p p:Propq:Proph✝:q⊢ q All goals completed! 🐙

(구조화되지 않은) cases는 같은 택틱으로 여러 하위 목표를 닫을 수 있을 때 특히 유용합니다:

example (p : Prop) : p ∨ p → p := p:Prop⊢ p ∨ p → p p:Proph:p ∨ p⊢ p p:Proph✝:p⊢ pp:Proph✝:p⊢ p repeat All goals completed! 🐙

또한 조합자 tac1 <;> tac2를 사용하면 택틱 tac1이 생성한 각 하위 목표에 tac2를 적용할 수 있습니다:

example (p : Prop) : p ∨ p → p := p:Prop⊢ p ∨ p → p p:Proph:p ∨ p⊢ p p:Proph✝:p⊢ pp:Proph✝:p⊢ p p:Proph✝:p⊢ pp:Proph✝:p⊢ p All goals completed! 🐙

구조화되지 않은 cases 택틱을 case 및 . 표기법과 결합하여 사용할 수 있습니다:

example (p q : Prop) : p ∨ q → q ∨ p := p:Propq:Prop⊢ p ∨ q → q ∨ p p:Propq:Proph:p ∨ q⊢ q ∨ p p:Propq:Proph✝:p⊢ q ∨ pp:Propq:Proph✝:q⊢ q ∨ p p:Propq:Proph✝:p⊢ q ∨ p p:Propq:Proph✝:p⊢ p All goals completed! 🐙 p:Propq:Proph✝:q⊢ q ∨ p p:Propq:Proph✝:q⊢ q All goals completed! 🐙 example (p q : Prop) : p ∨ q → q ∨ p := p:Propq:Prop⊢ p ∨ q → q ∨ p p:Propq:Proph:p ∨ q⊢ q ∨ p p:Propq:Proph✝:p⊢ q ∨ pp:Propq:Proph✝:q⊢ q ∨ p case inr h p:Propq:Proph:q⊢ q ∨ p p:Propq:Proph:q⊢ q All goals completed! 🐙 case inl h p:Propq:Proph:p⊢ q ∨ p p:Propq:Proph:p⊢ p All goals completed! 🐙 example (p q : Prop) : p ∨ q → q ∨ p := p:Propq:Prop⊢ p ∨ q → q ∨ p p:Propq:Proph:p ∨ q⊢ q ∨ p p:Propq:Proph✝:p⊢ q ∨ pp:Propq:Proph✝:q⊢ q ∨ p case inr h p:Propq:Proph:q⊢ q ∨ p p:Propq:Proph:q⊢ q All goals completed! 🐙 p:Propq:Proph✝:p⊢ q ∨ p p:Propq:Proph✝:p⊢ p All goals completed! 🐙

cases 택틱은 논리곱을 분해하는 데에도 사용할 수 있습니다:

example (p q : Prop) : p ∧ q → q ∧ p := p:Propq:Prop⊢ p ∧ q → q ∧ p p:Propq:Proph:p ∧ q⊢ q ∧ p cases h with p:Propq:Prophp:phq:q⊢ q ∧ p p:Propq:Prophp:phq:q⊢ qp:Propq:Prophp:phq:q⊢ p; p:Propq:Prophp:phq:q⊢ p; All goals completed! 🐙

이 예제에서는 cases 택틱을 적용한 후 목표가 하나만 남으며, h : p ∧ q는 hp : p와 hq : q라는 한 쌍의 가정으로 대체됩니다:

p:Propq:Prophp:phq:q⊢ q ∧ p

constructor 택틱은 논리곱의 유일한 생성자인 And.intro를 적용합니다.

이 택틱들을 사용하면 이전 절의 예제를 다음과 같이 다시 작성할 수 있습니다.

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) ↔ p ∧ q ∨ p ∧ r p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ rp:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)⊢ p ∧ q ∨ p ∧ r cases h with p:Propq:Propr:Prophp:phqr:q ∨ r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:ph✝:q⊢ p ∧ q ∨ p ∧ rp:Propq:Propr:Prophp:ph✝:r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:ph✝:q⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:ph✝:q⊢ p ∧ q; p:Propq:Propr:Prophp:ph✝:q⊢ pp:Propq:Propr:Prophp:ph✝:q⊢ q p:Propq:Propr:Prophp:ph✝:q⊢ pp:Propq:Propr:Prophp:ph✝:q⊢ q All goals completed! 🐙 p:Propq:Propr:Prophp:ph✝:r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:ph✝:r⊢ p ∧ r; p:Propq:Propr:Prophp:ph✝:r⊢ pp:Propq:Propr:Prophp:ph✝:r⊢ r p:Propq:Propr:Prophp:ph✝:r⊢ pp:Propq:Propr:Prophp:ph✝:r⊢ r All goals completed! 🐙 p:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ r⊢ p ∧ (q ∨ r) cases h with p:Propq:Propr:Prophpq:p ∧ q⊢ p ∧ (q ∨ r) cases hpq with p:Propq:Propr:Prophp:phq:q⊢ p ∧ (q ∨ r) p:Propq:Propr:Prophp:phq:q⊢ pp:Propq:Propr:Prophp:phq:q⊢ q ∨ r; p:Propq:Propr:Prophp:phq:q⊢ q ∨ r; p:Propq:Propr:Prophp:phq:q⊢ q; All goals completed! 🐙 p:Propq:Propr:Prophpr:p ∧ r⊢ p ∧ (q ∨ r) cases hpr with p:Propq:Propr:Prophp:phr:r⊢ p ∧ (q ∨ r) p:Propq:Propr:Prophp:phr:r⊢ pp:Propq:Propr:Prophp:phr:r⊢ q ∨ r; p:Propq:Propr:Prophp:phr:r⊢ q ∨ r; p:Propq:Propr:Prophp:phr:r⊢ r; All goals completed! 🐙

귀납적 타입에서 이 택틱들이 상당히 일반적이라는 것을 보게 될 것입니다. cases 택틱은 귀납적으로 정의된 타입의 어떤 원소든 분해하는 데 사용할 수 있으며, constructor는 귀납적으로 정의된 타입에서 적용 가능한 첫 번째 생성자를 항상 적용합니다. 예를 들어, 존재 한정자에 cases와 constructor를 사용할 수 있습니다:

example (p q : Nat → Prop) : (∃ x, p x) → ∃ x, p x ∨ q x := p:Nat → Propq:Nat → Prop⊢ (∃ x, p x) → ∃ x, p x ∨ q x p:Nat → Propq:Nat → Proph:∃ x, p x⊢ ∃ x, p x ∨ q x cases h with p:Nat → Propq:Nat → Propx:Natpx:p x⊢ ∃ x, p x ∨ q x p:Nat → Propq:Nat → Propx:Natpx:p x⊢ p ?intro.w ∨ q ?intro.wp:Nat → Propq:Nat → Propx:Natpx:p x⊢ Nat; p:Nat → Propq:Nat → Propx:Natpx:p x⊢ p ?intro.wp:Nat → Propq:Nat → Propx:Natpx:p x⊢ Nat; All goals completed! 🐙

여기서 constructor 택틱은 존재 주장의 첫 번째 구성 요소인 x의 값을 암시적으로 남겨 둡니다. 이 값은 메타변수로 표현되며, 이 메타변수는 나중에 인스턴스화되어야 합니다. 앞의 예제에서는 px의 타입이 p x이므로, 메타변수의 올바른 값이 exact px 택틱에 의해 결정됩니다. 존재 한정자에 대한 증거(witness)를 명시적으로 지정하고 싶다면, 대신 exists 택틱을 사용할 수 있습니다:

example (p q : Nat → Prop) : (∃ x, p x) → ∃ x, p x ∨ q x := p:Nat → Propq:Nat → Prop⊢ (∃ x, p x) → ∃ x, p x ∨ q x p:Nat → Propq:Nat → Proph:∃ x, p x⊢ ∃ x, p x ∨ q x cases h with p:Nat → Propq:Nat → Propx:Natpx:p x⊢ ∃ x, p x ∨ q x p:Nat → Propq:Nat → Propx:Natpx:p x⊢ p x ∨ q x; p:Nat → Propq:Nat → Propx:Natpx:p x⊢ p x; All goals completed! 🐙

다음은 또 다른 예입니다

example (p q : Nat → Prop) : (∃ x, p x ∧ q x) → ∃ x, q x ∧ p x := p:Nat → Propq:Nat → Prop⊢ (∃ x, p x ∧ q x) → ∃ x, q x ∧ p x p:Nat → Propq:Nat → Proph:∃ x, p x ∧ q x⊢ ∃ x, q x ∧ p x cases h with p:Nat → Propq:Nat → Propx:Nathpq:p x ∧ q x⊢ ∃ x, q x ∧ p x cases hpq with p:Nat → Propq:Nat → Propx:Nathp:p xhq:q x⊢ ∃ x, q x ∧ p x All goals completed! 🐙

이 택틱들은 명제뿐만 아니라 데이터에도 똑같이 사용할 수 있습니다. 다음 예제에서는 이 택틱들을 사용하여 곱 타입과 합 타입의 구성 요소를 서로 맞바꾸는 함수를 정의합니다:

def swap_pair : α × β → β × α := α:Type ?u.6β:Type ?u.5⊢ α × β → β × α α:Type ?u.6β:Type ?u.5p:α × β⊢ β × α α:Type ?u.6β:Type ?u.5fst✝:αsnd✝:β⊢ β × α α:Type ?u.6β:Type ?u.5fst✝:αsnd✝:β⊢ βα:Type ?u.6β:Type ?u.5fst✝:αsnd✝:β⊢ α α:Type ?u.6β:Type ?u.5fst✝:αsnd✝:β⊢ βα:Type ?u.6β:Type ?u.5fst✝:αsnd✝:β⊢ α All goals completed! 🐙 def swap_sum : Sum α β → Sum β α := α:Type ?u.6β:Type ?u.5⊢ α ⊕ β → β ⊕ α α:Type ?u.6β:Type ?u.5p:α ⊕ β⊢ β ⊕ α α:Type ?u.6β:Type ?u.5val✝:α⊢ β ⊕ αα:Type ?u.6β:Type ?u.5val✝:β⊢ β ⊕ α α:Type ?u.6β:Type ?u.5val✝:α⊢ β ⊕ α α:Type ?u.6β:Type ?u.5val✝:α⊢ α; All goals completed! 🐙 α:Type ?u.6β:Type ?u.5val✝:β⊢ β ⊕ α α:Type ?u.6β:Type ?u.5val✝:β⊢ β; All goals completed! 🐙

변수에 붙인 이름을 제외하면, 이 정의들은 논리곱과 논리합에 대한 유사한 명제들의 증명과 동일하다는 점에 유의하십시오. cases 택틱은 자연수에 대해서도 경우를 나눌 수 있습니다.

open Nat example (P : Nat → Prop) (h₀ : P 0) (h₁ : ∀ n, P (succ n)) (m : Nat) : P m := P:Nat → Proph₀:P 0h₁:∀ (n : Nat), P n.succm:Nat⊢ P m cases m with P:Nat → Proph₀:P 0h₁:∀ (n : Nat), P n.succ⊢ P 0 All goals completed! 🐙 P:Nat → Proph₀:P 0h₁:∀ (n : Nat), P n.succm':Nat⊢ P (m' + 1) All goals completed! 🐙

cases 택틱과 그 짝이 되는 induction 택틱은 귀납적 타입을 위한 택틱 절에서 더 자세히 다룹니다.

contradiction 택틱은 현재 목표의 가설 중에서 모순을 찾습니다:

example (p q : Prop) : p ∧ ¬ p → q := p:Propq:Prop⊢ p ∧ ¬p → q p:Propq:Proph:p ∧ ¬p⊢ q p:Propq:Propleft✝:pright✝:¬p⊢ q All goals completed! 🐙

택틱 블록에서도 match를 사용할 수 있습니다.

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) ↔ p ∧ q ∨ p ∧ r p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ rp:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)⊢ p ∧ q ∨ p ∧ r match h with p:Propq:Propr:Proph:p ∧ (q ∨ r)left✝:ph✝:q⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)left✝:ph✝:q⊢ p ∧ q; p:Propq:Propr:Proph:p ∧ (q ∨ r)left✝:ph✝:q⊢ pp:Propq:Propr:Proph:p ∧ (q ∨ r)left✝:ph✝:q⊢ q p:Propq:Propr:Proph:p ∧ (q ∨ r)left✝:ph✝:q⊢ pp:Propq:Propr:Proph:p ∧ (q ∨ r)left✝:ph✝:q⊢ q All goals completed! 🐙 p:Propq:Propr:Proph:p ∧ (q ∨ r)left✝:ph✝:r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)left✝:ph✝:r⊢ p ∧ r; p:Propq:Propr:Proph:p ∧ (q ∨ r)left✝:ph✝:r⊢ pp:Propq:Propr:Proph:p ∧ (q ∨ r)left✝:ph✝:r⊢ r p:Propq:Propr:Proph:p ∧ (q ∨ r)left✝:ph✝:r⊢ pp:Propq:Propr:Proph:p ∧ (q ∨ r)left✝:ph✝:r⊢ r All goals completed! 🐙 p:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ r⊢ p ∧ (q ∨ r) match h with p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhp:phq:q⊢ p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhp:phq:q⊢ pp:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhp:phq:q⊢ q ∨ r; p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhp:phq:q⊢ q ∨ r; p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhp:phq:q⊢ q; All goals completed! 🐙 p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhp:phr:r⊢ p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhp:phr:r⊢ pp:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhp:phr:r⊢ q ∨ r; p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhp:phr:r⊢ q ∨ r; p:Propq:Propr:Proph:p ∧ q ∨ p ∧ rhp:phr:r⊢ r; All goals completed! 🐙

intro와 match를 “결합”하여 앞의 예제들을 다음과 같이 작성할 수 있습니다:

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) ↔ p ∧ q ∨ p ∧ r p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ rp:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ r intro p:Propq:Propr:Propx✝:p ∧ (q ∨ r)hp:phq:q⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Propx✝:p ∧ (q ∨ r)hp:phq:q⊢ p ∧ q; p:Propq:Propr:Propx✝:p ∧ (q ∨ r)hp:phq:q⊢ pp:Propq:Propr:Propx✝:p ∧ (q ∨ r)hp:phq:q⊢ q p:Propq:Propr:Propx✝:p ∧ (q ∨ r)hp:phq:q⊢ pp:Propq:Propr:Propx✝:p ∧ (q ∨ r)hp:phq:q⊢ q All goals completed! 🐙 p:Propq:Propr:Propx✝:p ∧ (q ∨ r)hp:phr:r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Propx✝:p ∧ (q ∨ r)hp:phr:r⊢ p ∧ r; p:Propq:Propr:Propx✝:p ∧ (q ∨ r)hp:phr:r⊢ pp:Propq:Propr:Propx✝:p ∧ (q ∨ r)hp:phr:r⊢ r p:Propq:Propr:Propx✝:p ∧ (q ∨ r)hp:phr:r⊢ pp:Propq:Propr:Propx✝:p ∧ (q ∨ r)hp:phr:r⊢ r All goals completed! 🐙 p:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) intro p:Propq:Propr:Propx✝:p ∧ q ∨ p ∧ rhp:phq:q⊢ p ∧ (q ∨ r) p:Propq:Propr:Propx✝:p ∧ q ∨ p ∧ rhp:phq:q⊢ pp:Propq:Propr:Propx✝:p ∧ q ∨ p ∧ rhp:phq:q⊢ q ∨ r; p:Propq:Propr:Propx✝:p ∧ q ∨ p ∧ rhp:phq:q⊢ q ∨ r; p:Propq:Propr:Propx✝:p ∧ q ∨ p ∧ rhp:phq:q⊢ q; All goals completed! 🐙 p:Propq:Propr:Propx✝:p ∧ q ∨ p ∧ rhp:phr:r⊢ p ∧ (q ∨ r) p:Propq:Propr:Propx✝:p ∧ q ∨ p ∧ rhp:phr:r⊢ pp:Propq:Propr:Propx✝:p ∧ q ∨ p ∧ rhp:phr:r⊢ q ∨ r; p:Propq:Propr:Propx✝:p ∧ q ∨ p ∧ rhp:phr:r⊢ q ∨ r; p:Propq:Propr:Propx✝:p ∧ q ∨ p ∧ rhp:phr:r⊢ r; All goals completed! 🐙

5.4. 택틱 증명 구조화하기🔗

택틱은 증명을 구성하는 효율적인 방법을 제공하는 경우가 많지만, 명령이 길게 이어지면 논증의 구조가 잘 드러나지 않을 수 있습니다. 이 절에서는 택틱 스타일 증명에 구조를 부여하여 그러한 증명을 더 읽기 쉽고 견고하게 만드는 데 도움이 되는 몇 가지 수단을 설명합니다.

Lean의 증명 작성 구문이 갖는 좋은 점 하나는 항 스타일 증명과 택틱 스타일 증명을 섞어 쓸 수 있고, 둘 사이를 자유롭게 오갈 수 있다는 것입니다. 예를 들어 apply 택틱과 exact 택틱은 임의의 항을 받으며, 이 항은 have, show 등을 사용하여 작성할 수 있습니다. 반대로 임의의 Lean 항을 작성할 때에도 by 블록을 삽입하면 언제든지 택틱 모드를 호출할 수 있습니다. 다음은 다소 장난감 같은 예제입니다:

example (p q r : Prop) : p ∧ (q ∨ r) → (p ∧ q) ∨ (p ∧ r) := p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)⊢ p ∧ q ∨ p ∧ r exact have hp : p := h.left have hqr : q ∨ r := h.right show (p ∧ q) ∨ (p ∧ r) p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ r cases hqr with p:Propq:Propr:Proph:p ∧ (q ∨ r)hp:phq:q⊢ p ∧ q ∨ p ∧ r All goals completed! 🐙 p:Propq:Propr:Proph:p ∧ (q ∨ r)hp:phr:r⊢ p ∧ q ∨ p ∧ r All goals completed! 🐙

다음은 좀 더 자연스러운 예입니다.

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) ↔ p ∧ q ∨ p ∧ r p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ rp:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)⊢ p ∧ q ∨ p ∧ r cases h.right with p:Propq:Propr:Proph:p ∧ (q ∨ r)hq:q⊢ p ∧ q ∨ p ∧ r All goals completed! 🐙 p:Propq:Propr:Proph:p ∧ (q ∨ r)hr:r⊢ p ∧ q ∨ p ∧ r All goals completed! 🐙 p:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ r⊢ p ∧ (q ∨ r) cases h with p:Propq:Propr:Prophpq:p ∧ q⊢ p ∧ (q ∨ r) All goals completed! 🐙 p:Propq:Propr:Prophpr:p ∧ r⊢ p ∧ (q ∨ r) All goals completed! 🐙

사실 show 택틱도 있으며, 이는 증명 항의 show 표현식과 비슷합니다. 이 택틱은 택틱 모드를 유지한 채, 곧 해결할 목표의 타입을 선언하기만 합니다.

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) ↔ p ∧ q ∨ p ∧ r p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ rp:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)⊢ p ∧ q ∨ p ∧ r cases h.right with p:Propq:Propr:Proph:p ∧ (q ∨ r)hq:q⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)hq:q⊢ p ∧ q ∨ p ∧ r All goals completed! 🐙 p:Propq:Propr:Proph:p ∧ (q ∨ r)hr:r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)hr:r⊢ p ∧ q ∨ p ∧ r All goals completed! 🐙 p:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ r⊢ p ∧ (q ∨ r) cases h with p:Propq:Propr:Prophpq:p ∧ q⊢ p ∧ (q ∨ r) p:Propq:Propr:Prophpq:p ∧ q⊢ p ∧ (q ∨ r) All goals completed! 🐙 p:Propq:Propr:Prophpr:p ∧ r⊢ p ∧ (q ∨ r) p:Propq:Propr:Prophpr:p ∧ r⊢ p ∧ (q ∨ r) All goals completed! 🐙

사실 show 택틱을 사용하면 목표를 정의적으로 같은 다른 형태로 다시 쓸 수도 있습니다:

example (n : Nat) : n + 1 = Nat.succ n := n:Nat⊢ n + 1 = n.succ n:Nat⊢ n.succ = n.succ All goals completed! 🐙

또한 증명 항을 작성할 때와 마찬가지로 새로운 하위 목표를 도입하는 have 택틱도 있습니다:

example (p q r : Prop) : p ∧ (q ∨ r) → (p ∧ q) ∨ (p ∧ r) := p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phqr:q ∨ r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phqr:q ∨ r⊢ p ∧ q ∨ p ∧ r cases hqr with p:Propq:Propr:Prophp:phq:q⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phq:qhpq:p ∧ q⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phq:qhpq:p ∧ q⊢ p ∧ q All goals completed! 🐙 p:Propq:Propr:Prophp:phr:r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phr:rhpr:p ∧ r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phr:rhpr:p ∧ r⊢ p ∧ r All goals completed! 🐙

증명 항에서와 마찬가지로 have 택틱에서도 레이블을 생략할 수 있으며, 이 경우 기본 레이블 this가 사용됩니다:

example (p q r : Prop) : p ∧ (q ∨ r) → (p ∧ q) ∨ (p ∧ r) := p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phqr:q ∨ r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phqr:q ∨ r⊢ p ∧ q ∨ p ∧ r cases hqr with p:Propq:Propr:Prophp:phq:q⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phq:qthis:p ∧ q⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phq:qthis:p ∧ q⊢ p ∧ q All goals completed! 🐙 p:Propq:Propr:Prophp:phr:r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phr:rthis:p ∧ r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phr:rthis:p ∧ r⊢ p ∧ r All goals completed! 🐙

have 택틱에서는 타입을 생략할 수 있으므로 have hp := h.left와 have hqr := h.right처럼 쓸 수 있습니다. 사실 이 표기법에서는 타입과 레이블을 모두 생략할 수도 있으며, 이 경우 새로운 사실은 this라는 레이블로 도입됩니다:

example (p q r : Prop) : p ∧ (q ∨ r) → (p ∧ q) ∨ (p ∧ r) := p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phqr:q ∨ r⊢ p ∧ q ∨ p ∧ r cases hqr with p:Propq:Propr:Prophp:phq:q⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phq:qthis:p ∧ q⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phq:qthis:p ∧ q⊢ p ∧ q; All goals completed! 🐙 p:Propq:Propr:Prophp:phr:r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phr:rthis:p ∧ r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Prophp:phr:rthis:p ∧ r⊢ p ∧ r; All goals completed! 🐙

Lean에는 let 택틱도 있습니다. 이 택틱은 have 택틱과 비슷하지만, 보조 사실 대신 지역 정의를 도입하는 데 사용됩니다. 이는 증명 항에서의 let에 대응하는 택틱입니다:

example : ∃ x, x + 2 = 8 := ⊢ ∃ x, x + 2 = 8 a:Nat := 3 * 2⊢ ∃ x, x + 2 = 8 All goals completed! 🐙

have와 마찬가지로 let a := 3 * 2와 같이 작성하여 타입을 암시적으로 남겨 둘 수 있습니다. let과 have의 차이점은 let이 문맥에 지역 정의를 도입하므로 증명 안에서 해당 지역 선언의 정의를 펼칠 수 있다는 것입니다.

지금까지 .를 사용하여 중첩된 택틱 블록을 만들었습니다. 중첩된 블록 안에서 Lean은 첫 번째 목표에 집중하며, 블록이 끝날 때 그 목표가 완전히 해결되지 않았으면 오류를 발생시킵니다. 이는 한 택틱이 만들어 낸 여러 하위 목표 각각의 증명을 구분해서 나타낼 때 유용합니다. . 표기법은 공백에 민감하며, 들여쓰기를 보고 택틱 블록이 끝나는지 판단합니다. 또는 중괄호와 세미콜론을 사용하여 택틱 블록을 정의할 수도 있습니다:

example (p q r : Prop) : p ∧ (q ∨ r) ↔ (p ∧ q) ∨ (p ∧ r) := p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) ↔ p ∧ q ∨ p ∧ r p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ rp:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Prop⊢ p ∧ (q ∨ r) → p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)⊢ p ∧ q ∨ p ∧ r; p:Propq:Propr:Proph:p ∧ (q ∨ r)h✝:q⊢ p ∧ q ∨ p ∧ rp:Propq:Propr:Proph:p ∧ (q ∨ r)h✝:r⊢ p ∧ q ∨ p ∧ r; p:Propq:Propr:Proph:p ∧ (q ∨ r)h✝:q⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)h✝:q⊢ p ∧ q ∨ p ∧ r; All goals completed! 🐙 } p:Propq:Propr:Proph:p ∧ (q ∨ r)h✝:r⊢ p ∧ q ∨ p ∧ r p:Propq:Propr:Proph:p ∧ (q ∨ r)h✝:r⊢ p ∧ q ∨ p ∧ r; All goals completed! 🐙 } } p:Propq:Propr:Prop⊢ p ∧ q ∨ p ∧ r → p ∧ (q ∨ r) p:Propq:Propr:Proph:p ∧ q ∨ p ∧ r⊢ p ∧ (q ∨ r); p:Propq:Propr:Proph✝:p ∧ q⊢ p ∧ (q ∨ r)p:Propq:Propr:Proph✝:p ∧ r⊢ p ∧ (q ∨ r); p:Propq:Propr:Proph✝:p ∧ q⊢ p ∧ (q ∨ r) p:Propq:Propr:Proph✝:p ∧ q⊢ p ∧ (q ∨ r); p:Propq:Propr:Prophpq:p ∧ q⊢ p ∧ (q ∨ r); All goals completed! 🐙 } p:Propq:Propr:Proph✝:p ∧ r⊢ p ∧ (q ∨ r) p:Propq:Propr:Proph✝:p ∧ r⊢ p ∧ (q ∨ r); p:Propq:Propr:Prophpr:p ∧ r⊢ p ∧ (q ∨ r); All goals completed! 🐙 } }

들여쓰기를 사용하여 증명을 구조화하면 유용합니다. 택틱이 둘 이상의 하위 목표를 남길 때마다, 남은 하위 목표들을 블록으로 감싸고 들여써서 서로 구분합니다. 따라서 정리 foo를 하나의 목표에 적용하여 네 개의 하위 목표가 생긴다면, 증명은 다음과 같은 모습일 것이라고 예상할 수 있습니다.

  apply foo
  . <proof of first goal>
  . <proof of second goal>
  . <proof of third goal>
  . <proof of final goal>

또는

  apply foo
  case <tag of first goal>  => <proof of first goal>
  case <tag of second goal> => <proof of second goal>
  case <tag of third goal>  => <proof of third goal>
  case <tag of final goal>  => <proof of final goal>

또는

  apply foo
  { <proof of first goal>  }
  { <proof of second goal> }
  { <proof of third goal>  }
  { <proof of final goal>  }

5.5. 택틱 조합자🔗

택틱 조합자는 기존 택틱으로부터 새로운 택틱을 만드는 연산입니다. 순차 조합자는 by 블록에 이미 암묵적으로 포함되어 있습니다:

example (p q : Prop) (hp : p) : p ∨ q := p:Propq:Prophp:p⊢ p ∨ q p:Propq:Prophp:p⊢ p; All goals completed! 🐙

여기서 apply Or.inl; assumption은 먼저 apply Or.inl을 적용한 다음 assumption을 적용하는 단일 택틱과 기능적으로 동등합니다.

t₁ <;> t₂에서 <;> 연산자는 순차 실행 연산의 병렬 버전을 제공합니다. t₁이 현재 목표에 적용된 다음, 그 결과로 생긴 모든 하위 목표에 t₂가 적용됩니다:

example (p q : Prop) (hp : p) (hq : q) : p ∧ q := p:Propq:Prophp:phq:q⊢ p ∧ q p:Propq:Prophp:phq:q⊢ pp:Propq:Prophp:phq:q⊢ q p:Propq:Prophp:phq:q⊢ pp:Propq:Prophp:phq:q⊢ q All goals completed! 🐙

이는 결과로 생긴 목표들을 일관된 방식으로 마무리할 수 있을 때, 또는 적어도 모든 목표를 일관되게 진전시킬 수 있을 때 특히 유용합니다.

first | t₁ | t₂ | ... | tₙ는 하나가 성공할 때까지 각 tᵢ를 차례로 적용하며, 모두 성공하지 못하면 실패합니다:

example (p q : Prop) (hp : p) : p ∨ q := p:Propq:Prophp:p⊢ p ∨ q first | p:Propq:Prophp:p⊢ p; All goals completed! 🐙 | apply Or.inr; assumption example (p q : Prop) (hq : q) : p ∨ q := p:Propq:Prophq:q⊢ p ∨ q first | p:Propq:Prophq:q⊢ p; p:Propq:Prophq:q⊢ p | p:Propq:Prophq:q⊢ q; All goals completed! 🐙

첫 번째 예제에서는 왼쪽 분기가 성공하는 반면, 두 번째 예제에서는 오른쪽 분기가 성공합니다. 다음 세 예제에서는 동일한 복합 택틱이 모든 경우에 성공합니다:

example (p q r : Prop) (hp : p) : p ∨ q ∨ r := p:Propq:Propr:Prophp:p⊢ p ∨ q ∨ r repeat (first | p:Propq:Propr:Prophp:p⊢ p; All goals completed! 🐙 | apply Or.inr | assumption) example (p q r : Prop) (hq : q) : p ∨ q ∨ r := p:Propq:Propr:Prophq:q⊢ p ∨ q ∨ r repeat (first | p:Propq:Propr:Prophq:q⊢ q; All goals completed! 🐙 | p:Propq:Propr:Prophq:q⊢ q ∨ r | assumption) example (p q r : Prop) (hr : r) : p ∨ q ∨ r := p:Propq:Propr:Prophr:r⊢ p ∨ q ∨ r repeat (first | p:Propq:Propr:Prophr:r⊢ r; p:Propq:Propr:Prophr:r⊢ q | p:Propq:Propr:Prophr:r⊢ r | All goals completed! 🐙)

이 택틱은 먼저 왼쪽 논리합 항을 가정으로 즉시 풀려고 시도합니다. 이것이 실패하면 오른쪽 논리합 항에 집중하려고 시도하고, 그것도 통하지 않으면 assumption 택틱을 호출합니다.

지금쯤이면 택틱이 실패할 수 있다는 것을 분명히 눈치채셨을 것입니다. 실제로 first 조합자가 백트래킹하여 다음 택틱을 시도하게 만드는 것이 바로 이 “실패” 상태입니다. try 조합자는 사소한 방식일 수도 있지만 항상 성공하는 택틱을 만듭니다. try t는 t를 실행하고, t가 실패하더라도 성공을 보고합니다. 이는 first| t |skip과 동등하며, 여기서 skip은 아무것도 하지 않는(그리고 그렇게 하는 데 성공하는) 택틱입니다. 다음 예제에서 두 번째 constructor는 오른쪽 논리곱 q ∧ r에서는 성공하지만(논리합과 논리곱은 오른쪽으로 결합한다는 것을 기억하십시오) 첫 번째 논리곱에서는 실패합니다. try 택틱은 순차 합성이 성공하도록 보장합니다:

example (p q r : Prop) (hp : p) (hq : q) (hr : r) : p ∧ q ∧ r := p:Propq:Propr:Prophp:phq:qhr:r⊢ p ∧ q ∧ r p:Propq:Propr:Prophp:phq:qhr:r⊢ pp:Propq:Propr:Prophp:phq:qhr:r⊢ q ∧ r p:Propq:Propr:Prophp:phq:qhr:r⊢ pp:Propq:Propr:Prophp:phq:qhr:r⊢ q ∧ r (try p:Propq:Propr:Prophp:phq:qhr:r⊢ qp:Propq:Propr:Prophp:phq:qhr:r⊢ r) p:Propq:Propr:Prophp:phq:qhr:r⊢ pp:Propq:Propr:Prophp:phq:qhr:r⊢ qp:Propq:Propr:Prophp:phq:qhr:r⊢ r All goals completed! 🐙

주의하십시오: repeat (try t)는 내부 택틱이 절대 실패하지 않으므로 끝없이 반복됩니다.

증명을 하다 보면 아직 해결되지 않은 목표가 여러 개 남아 있는 경우가 많습니다. 병렬 순차 결합은 하나의 택틱이 여러 목표에 적용되도록 구성하는 방법 중 하나이지만, 다른 방법도 있습니다. 예를 들어, all_goals t는 열려 있는 모든 목표에 t를 적용합니다:

example (p q r : Prop) (hp : p) (hq : q) (hr : r) : p ∧ q ∧ r := p:Propq:Propr:Prophp:phq:qhr:r⊢ p ∧ q ∧ r p:Propq:Propr:Prophp:phq:qhr:r⊢ pp:Propq:Propr:Prophp:phq:qhr:r⊢ q ∧ r all_goals (try p:Propq:Propr:Prophp:phq:qhr:r⊢ qp:Propq:Propr:Prophp:phq:qhr:r⊢ r) all_goals All goals completed! 🐙

이 경우에는 any_goals 택틱이 더 견고한 해결책을 제공합니다. 이 택틱은 all_goals와 비슷하지만, 인수가 적어도 하나의 목표에서 성공하면 성공한다는 점이 다릅니다:

example (p q r : Prop) (hp : p) (hq : q) (hr : r) : p ∧ q ∧ r := p:Propq:Propr:Prophp:phq:qhr:r⊢ p ∧ q ∧ r p:Propq:Propr:Prophp:phq:qhr:r⊢ pp:Propq:Propr:Prophp:phq:qhr:r⊢ q ∧ r any_goals p:Propq:Propr:Prophp:phq:qhr:r⊢ qp:Propq:Propr:Prophp:phq:qhr:r⊢ r any_goals All goals completed! 🐙

아래 by 블록의 첫 번째 택틱은 논리곱을 반복해서 분해합니다:

example (p q r : Prop) (hp : p) (hq : q) (hr : r) : p ∧ ((p ∧ q) ∧ r) ∧ (q ∧ r ∧ p) := p:Propq:Propr:Prophp:phq:qhr:r⊢ p ∧ ((p ∧ q) ∧ r) ∧ q ∧ r ∧ p repeat (any_goals p:Propq:Propr:Prophp:phq:qhr:r⊢ p) all_goals All goals completed! 🐙

사실, 전체 택틱을 한 줄로 압축할 수도 있습니다:

example (p q r : Prop) (hp : p) (hq : q) (hr : r) : p ∧ ((p ∧ q) ∧ r) ∧ (q ∧ r ∧ p) := p:Propq:Propr:Prophp:phq:qhr:r⊢ p ∧ ((p ∧ q) ∧ r) ∧ q ∧ r ∧ p repeat (any_goals (first | p:Propq:Propr:Prophp:phq:qhr:r⊢ p | All goals completed! 🐙))

조합자 focus t는 t가 현재 목표에만 영향을 미치도록 보장하며, 나머지 목표는 일시적으로 범위에서 숨깁니다. 따라서 t가 원래 현재 목표에만 영향을 미친다면, focus (all_goals t)는 t와 같은 효과를 냅니다.

5.6. 재작성🔗

rw 택틱과 simp 택틱은 계산적 증명에서 간략히 소개했습니다. 이번 절과 다음 절에서는 이 두 택틱을 더 자세히 살펴봅니다.

rw 택틱은 목표와 가설에 치환을 적용하는 기본 메커니즘을 제공하며, 상등을 다루는 편리하고 효율적인 방법을 제공합니다. 이 택틱의 가장 기본적인 형태는 rw [t]이며, 여기서 t는 그 타입이 상등을 주장하는 항입니다. 예를 들어, t는 문맥에 있는 가설 h : x = y일 수 있습니다. add_comm : ∀ x y, x + y = y + x와 같은 일반적인 보조정리일 수도 있으며, 이 경우 rewrite 택틱은 x와 y를 적절히 인스턴스화할 방법을 찾으려고 시도합니다. 또는 구체적이거나 일반적인 등식을 나타내는 임의의 복합 항일 수도 있습니다. 다음 예제에서는 이 기본 형태를 사용하여 가설로 목표를 다시 씁니다.

variable (k : Nat) (f : Nat → Nat) example (h₁ : f 0 = 0) (h₂ : k = 0) : f k = 0 := k:Natf:Nat → Nath₁:f 0 = 0h₂:k = 0⊢ f k = 0 k:Natf:Nat → Nath₁:f 0 = 0h₂:k = 0⊢ f 0 = 0 -- replace k with 0 All goals completed! 🐙 -- replace f 0 with 0

위 예제에서 rw을 처음 사용하면 목표 f k = 0의 k가 0으로 치환됩니다. 이어서 두 번째 사용에서는 f 0이 0으로 치환됩니다. 이 택틱은 t = t 형태의 목표를 모두 자동으로 닫습니다. 다음은 복합 표현식을 사용하여 재작성하는 예제입니다:

example (x y : Nat) (p : Nat → Prop) (q : Prop) (h : q → x = y) (h' : p y) (hq : q) : p x := x:Naty:Natp:Nat → Propq:Proph:q → x = yh':p yhq:q⊢ p x x:Naty:Natp:Nat → Propq:Proph:q → x = yh':p yhq:q⊢ p y; All goals completed! 🐙

여기서 h hq는 등식 x = y를 성립시킵니다.

rw [t_1, ..., t_n] 표기법을 사용하면 여러 번의 다시 쓰기를 하나로 결합할 수 있습니다. 이 표기법은 rw[t_1]; ...;rw [t_n]의 축약형일 뿐입니다. 앞의 예제는 다음과 같이 작성할 수 있습니다.

variable (k : Nat) (f : Nat → Nat) example (h₁ : f 0 = 0) (h₂ : k = 0) : f k = 0 := k:Natf:Nat → Nath₁:f 0 = 0h₂:k = 0⊢ f k = 0 All goals completed! 🐙

기본적으로 rw는 방정식을 정방향으로 사용하여, 좌변을 표현식과 매칭한 뒤 그것을 우변으로 대체합니다. ←t 표기법을 사용하면 택틱이 상등 t를 역방향으로 사용하도록 지시할 수 있습니다.

variable (a b : Nat) (f : Nat → Nat) example (h₁ : a = b) (h₂ : f a = 0) : f b = 0 := a:Natb:Natf:Nat → Nath₁:a = bh₂:f a = 0⊢ f b = 0 All goals completed! 🐙

이 예제에서 ←h₁ 항은 재작성기에게 b를 a로 바꾸도록 지시합니다. 편집기에서는 역방향 화살표를 \l로 입력할 수 있습니다. 이에 대응하는 ASCII 표기인 <-를 사용할 수도 있습니다.

항등식의 좌변이 패턴 안에서 둘 이상의 부분 항과 일치할 때가 있습니다. 이 경우 rw 택틱은 항을 순회하면서 처음 발견한 일치 항목을 선택합니다. 그 항목이 원하는 것이 아니라면 추가 인수를 사용하여 적절한 부분 항을 지정할 수 있습니다.

example (a b c : Nat) : a + b + c = a + c + b := a:Natb:Natc:Nat⊢ a + b + c = a + c + b All goals completed! 🐙 example (a b c : Nat) : a + b + c = a + c + b := a:Natb:Natc:Nat⊢ a + b + c = a + c + b All goals completed! 🐙 example (a b c : Nat) : a + b + c = a + c + b := a:Natb:Natc:Nat⊢ a + b + c = a + c + b All goals completed! 🐙

위의 첫 번째 예제에서 첫 단계는 a + b + c를 a + (b + c)로 재작성합니다. 다음 단계는 항 b + c에 교환법칙을 적용합니다. 인수를 지정하지 않으면 이 택틱은 그 대신 a + (b + c)를 (b + c) + a로 재작성하게 됩니다. 마지막 단계에서는 결합법칙을 역방향으로 적용하여 a + (c + b)를 a + c + b로 재작성합니다. 이어지는 두 예제는 이와 달리 결합법칙을 적용하여 양변의 괄호를 오른쪽으로 옮긴 다음, b와 c를 서로 바꿉니다. 마지막 예제에서는 Nat.add_comm의 두 번째 인수를 지정하여 재작성이 우변에서 일어나도록 지정한다는 점에 주목하십시오.

기본적으로 rw 택틱은 목표에만 적용됩니다. rw [t] at h 표기법은 재작성을 적용합니다

example (f : Nat → Nat) (a : Nat) (h : a + 0 = 0) : f a = f 0 := f:Nat → Nata:Nath:a + 0 = 0⊢ f a = f 0 f:Nat → Nata:Nath:a = 0⊢ f a = f 0 All goals completed! 🐙

첫 번째 단계인 rw [Nat.add_zero] at h는 가설 a + 0 = 0을 a = 0으로 다시 씁니다. 그런 다음 새 가설 a = 0을 사용하여 목표를 f 0 = f 0으로 다시 씁니다.

rw 택틱은 명제에만 쓰이는 것이 아닙니다. 다음 예제에서는 rw [h] at t를 사용하여 가설 t : Tuple α n을 t : Tuple α 0으로 다시 씁니다.

def Tuple (α : Type) (n : Nat) := { as : List α // as.length = n } example (n : Nat) (h : n = 0) (t : Tuple α n) : Tuple α 0 := α:Typen:Nath:n = 0t:Tuple α n⊢ Tuple α 0 α:Typen:Nath:n = 0t:Tuple α 0⊢ Tuple α 0 All goals completed! 🐙

5.7. 단순화기 사용하기🔗

rw는 목표를 조작하기 위한 정밀한 도구로 설계된 반면, 단순화기(simplifier)는 더 강력한 형태의 자동화를 제공합니다. Lean의 라이브러리에 있는 여러 항등식에는 [simp] 속성이 붙어 있으며, simp 택틱은 이를 사용하여 표현식의 부분 항을 반복적으로 재작성합니다.

example (x y z : Nat) : (x + 0) * (0 + y * 1 + z * 0) = x * y := x:Naty:Natz:Nat⊢ (x + 0) * (0 + y * 1 + z * 0) = x * y All goals completed! 🐙 example (x y z : Nat) (p : Nat → Prop) (h : p (x * y)) : p ((x + 0) * (0 + y * 1 + z * 0)) := x:Naty:Natz:Natp:Nat → Proph:p (x * y)⊢ p ((x + 0) * (0 + y * 1 + z * 0)) x:Naty:Natz:Natp:Nat → Proph:p (x * y)⊢ p (x * y); All goals completed! 🐙

첫 번째 예제에서는 목표에 있는 상등의 좌변이 0과 1에 관한 일반적인 항등식을 사용하여 단순화되며, 이에 따라 목표는 x * y = x * y로 축약됩니다. 이 시점에서 simp가 반사성을 적용하여 증명을 마무리합니다. 두 번째 예제에서는 simp가 목표를 p (x * y)로 축소하며, 이 시점에서 가정 h가 증명을 마무리합니다. 다음은 리스트를 사용하는 몇 가지 예제입니다:

open List example (xs : List Nat) : reverse (xs ++ [1, 2, 3]) = [3, 2, 1] ++ reverse xs := xs:List Nat⊢ (xs ++ [1, 2, 3]).reverse = [3, 2, 1] ++ xs.reverse All goals completed! 🐙 example (xs ys : List α) : length (reverse (xs ++ ys)) = length xs + length ys := α:Type u_1xs:List αys:List α⊢ (xs ++ ys).reverse.length = xs.length + ys.length All goals completed! 🐙

rw와 마찬가지로, at 키워드를 사용하여 가설을 단순화할 수 있습니다:

example (x y z : Nat) (p : Nat → Prop) (h : p ((x + 0) * (0 + y * 1 + z * 0))) : p (x * y) := x:Naty:Natz:Natp:Nat → Proph:p ((x + 0) * (0 + y * 1 + z * 0))⊢ p (x * y) x:Naty:Natz:Natp:Nat → Proph:p (x * y)⊢ p (x * y); All goals completed! 🐙

또한 “와일드카드” 별표를 사용하면 모든 가설과 목표를 단순화할 수 있습니다:

attribute [local simp] Nat.mul_comm Nat.mul_assoc Nat.mul_left_comm attribute [local simp] Nat.add_assoc Nat.add_comm Nat.add_left_comm example (w x y z : Nat) (p : Nat → Prop) (h : p (x * y + z * w * x)) : p (x * w * z + y * x) := w:Natx:Naty:Natz:Natp:Nat → Proph:p (x * y + z * w * x)⊢ p (x * w * z + y * x) w:Natx:Naty:Natz:Natp:Nat → Proph:p (x * y + w * (x * z))⊢ p (x * y + w * (x * z)); All goals completed! 🐙 example (x y z : Nat) (p : Nat → Prop) (h₁ : p (1 * x + y)) (h₂ : p (x * z * 1)) : p (y + 0 + x) ∧ p (z * x) := x:Naty:Natz:Natp:Nat → Proph₁:p (1 * x + y)h₂:p (x * z * 1)⊢ p (y + 0 + x) ∧ p (z * x) x:Naty:Natz:Natp:Nat → Proph₁:p (x + y)h₂:p (x * z)⊢ p (x + y) ∧ p (x * z) x:Naty:Natz:Natp:Nat → Proph₁:p (x + y)h₂:p (x * z)⊢ p (x + y) ∧ p (x * z) x:Naty:Natz:Natp:Nat → Proph₁:p (x + y)h₂:p (x * z)⊢ p (x + y)x:Naty:Natz:Natp:Nat → Proph₁:p (x + y)h₂:p (x * z)⊢ p (x * z) x:Naty:Natz:Natp:Nat → Proph₁:p (x + y)h₂:p (x * z)⊢ p (x + y)x:Naty:Natz:Natp:Nat → Proph₁:p (x + y)h₂:p (x * z)⊢ p (x * z) All goals completed! 🐙

자연수의 곱셈처럼 교환법칙과 결합법칙이 성립하는 연산에 대해, 단순화기는 이 두 사실과 함께 좌교환법칙(left commutativity)을 사용하여 표현식을 재작성합니다. 곱셈의 경우 좌교환성은 다음과 같이 표현됩니다: x * (y * z) = y * (x * z). local 수식어는 현재 파일(또는 경우에 따라 현재 섹션이나 네임스페이스)에서 이 규칙들을 사용하도록 단순화기에 지시합니다. 교환법칙과 좌교환성은 어느 쪽이든 반복해서 적용하면 무한 루프가 발생한다는 점에서 문제가 있어 보일 수 있습니다. 하지만 단순화기는 인자의 순서를 바꾸는 항등식을 감지하고, 순서 있는 재작성이라는 기법을 사용합니다. 이는 시스템이 항들에 대한 내부 순서를 유지하며, 항등식을 적용했을 때 그 순서가 감소하는 경우에만 항등식을 적용한다는 뜻입니다. 위에서 언급한 세 가지 항등식을 사용하면, 표현식의 모든 괄호가 오른쪽으로 결합되고 표현식들이 표준적인(다소 임의적이기는 하지만) 방식으로 정렬되는 효과가 있습니다. 그러면 결합법칙과 교환법칙을 제외하면 동치인 두 표현식은 같은 표준형으로 재작성됩니다.

example (w x y z : Nat) (Variable name `p` 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] _p Note: This linter can be disabled with `set_option linter.unusedVariables false`p : Nat → Prop) : x * y + z * w * x = x * w * z + y * x := w:Natx:Naty:Natz:Natp:Nat → Prop⊢ x * y + z * w * x = x * w * z + y * x All goals completed! 🐙 example (w x y z : Nat) (p : Nat → Prop) (h : p (x * y + z * w * x)) : p (x * w * z + y * x) := w:Natx:Naty:Natz:Natp:Nat → Proph:p (x * y + z * w * x)⊢ p (x * w * z + y * x) w:Natx:Naty:Natz:Natp:Nat → Proph:p (x * y + z * w * x)⊢ p (x * y + w * (x * z)); w:Natx:Naty:Natz:Natp:Nat → Proph:p (x * y + w * (x * z))⊢ p (x * y + w * (x * z)); All goals completed! 🐙

rw와 마찬가지로 simp에도 사용할 사실들의 목록을 넘길 수 있으며, 여기에는 일반 보조정리, 지역 가정, 펼칠 정의, 복합 표현식이 포함됩니다. simp 택틱은 rewrite와 마찬가지로 ←t 구문도 인식합니다. 어느 경우든 추가된 규칙들은 항을 단순화하는 데 사용되는 항등식 모음에 더해집니다.

def f (m n : Nat) : Nat := m + n + m example {m n : Nat} (h : n = 1) (h' : 0 = m) : (f m n) = n := m:Natn:Nath:n = 1h':0 = m⊢ f m n = n All goals completed! 🐙

흔히 쓰는 관용적 방법은 지역 가정을 사용해 목표를 단순화하는 것입니다:

variable (k : Nat) (f : Nat → Nat) example (h₁ : f 0 = 0) (h₂ : k = 0) : f k = 0 := k:Natf:Nat → Nath₁:f 0 = 0h₂:k = 0⊢ f k = 0 All goals completed! 🐙

단순화할 때 로컬 컨텍스트에 있는 모든 가설을 사용하려면 와일드카드 기호 *를 사용할 수 있습니다:

variable (k : Nat) (f : Nat → Nat) example (h₁ : f 0 = 0) (h₂ : k = 0) : f k = 0 := k:Natf:Nat → Nath₁:f 0 = 0h₂:k = 0⊢ f k = 0 All goals completed! 🐙

다음은 또 다른 예입니다:

example (u w x y z : Nat) (h₁ : x = y + z) (h₂ : w = u + x) : w = z + y + u := u:Natw:Natx:Naty:Natz:Nath₁:x = y + zh₂:w = u + x⊢ w = z + y + u All goals completed! 🐙

단순화기는 명제 재작성도 수행합니다. 예를 들어, 단순화기는 가설 p를 사용하여 p ∧ q를 q로, p ∨ q를 True로 재작성한 다음, 이를 자명하게 증명합니다. 이러한 재작성을 반복하면 자명하지 않은 명제 추론을 해낼 수 있습니다.

example (p q : Prop) (hp : p) : p ∧ q ↔ q := p:Propq:Prophp:p⊢ p ∧ q ↔ q All goals completed! 🐙 example (p q : Prop) (hp : p) : p ∨ q := p:Propq:Prophp:p⊢ p ∨ q All goals completed! 🐙 example (p q r : Prop) (hp : p) (hq : q) : p ∧ (q ∨ r) := p:Propq:Propr:Prophp:phq:q⊢ p ∧ (q ∨ r) All goals completed! 🐙

다음 예제는 모든 가설을 단순화한 다음, 이를 사용하여 목표를 증명합니다.

example (u w x x' y y' z : Nat) (p : Nat → Prop) (h₁ : x + 0 = x') (h₂ : y + 0 = y') : x + y + 0 = x' + y' := u:Natw:Natx:Natx':Naty:Naty':Natz:Natp:Nat → Proph₁:x + 0 = x'h₂:y + 0 = y'⊢ x + y + 0 = x' + y' u:Natw:Natx:Natx':Naty:Naty':Natz:Natp:Nat → Proph₁:x = x'h₂:y = y'⊢ x + y = x' + y' All goals completed! 🐙

단순화기(simplifier)가 특히 유용한 이유 중 하나는 라이브러리가 발전함에 따라 그 능력도 함께 확장될 수 있다는 점입니다. 예를 들어, 입력 리스트 뒤에 그 역순을 이어 붙여 대칭으로 만드는 리스트 연산을 다음과 같이 정의한다고 가정해 봅시다.

def mk_symm (xs : List α) := xs ++ xs.reverse

그러면 임의의 리스트 xs에 대해 (mk_symm xs).reverse는 mk_symm xs와 같으며, 이는 정의를 펼쳐 쉽게 증명할 수 있습니다:

theorem reverse_mk_symm (xs : List α) : (mk_symm xs).reverse = mk_symm xs := α:Type u_1xs:List α⊢ (mk_symm xs).reverse = mk_symm xs All goals completed! 🐙

이제 이 정리를 사용하여 새로운 결과를 증명할 수 있습니다:

example (xs ys : List Nat) : (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse := xs:List Natys:List Nat⊢ (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse All goals completed! 🐙 example (xs ys : List Nat) (p : List Nat → Prop) (h : p (xs ++ mk_symm ys).reverse) : p (mk_symm ys ++ xs.reverse) := xs:List Natys:List Natp:List Nat → Proph:p (xs ++ mk_symm ys).reverse⊢ p (mk_symm ys ++ xs.reverse) xs:List Natys:List Natp:List Nat → Proph:p (mk_symm ys ++ xs.reverse)⊢ p (mk_symm ys ++ xs.reverse); All goals completed! 🐙

하지만 일반적으로는 reverse_mk_symm을 사용하는 것이 옳으며, 사용자가 이를 명시적으로 호출하지 않아도 된다면 좋을 것입니다. 정리를 정의할 때 이를 단순화 규칙으로 표시하면 그렇게 할 수 있습니다:

@[simp] theorem reverse_mk_symm (xs : List α) : (mk_symm xs).reverse = mk_symm xs := α:Type u_1xs:List α⊢ (mk_symm xs).reverse = mk_symm xs All goals completed! 🐙 example (xs ys : List Nat) : (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse := xs:List Natys:List Nat⊢ (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse All goals completed! 🐙 example (xs ys : List Nat) (p : List Nat → Prop) (h : p (xs ++ mk_symm ys).reverse) : p (mk_symm ys ++ xs.reverse) := xs:List Natys:List Natp:List Nat → Proph:p (xs ++ mk_symm ys).reverse⊢ p (mk_symm ys ++ xs.reverse) xs:List Natys:List Natp:List Nat → Proph:p (mk_symm ys ++ xs.reverse)⊢ p (mk_symm ys ++ xs.reverse); All goals completed! 🐙

@[simp] 표기법은 reverse_mk_symm이 [simp] 속성을 가지도록 선언하며, 다음과 같이 더 명시적으로 풀어 쓸 수도 있습니다:

theorem reverse_mk_symm (xs : List α) : (mk_symm xs).reverse = mk_symm xs := α:Type u_1xs:List α⊢ (mk_symm xs).reverse = mk_symm xs All goals completed! 🐙 attribute [simp] reverse_mk_symm example (xs ys : List Nat) : (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse := xs:List Natys:List Nat⊢ (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse All goals completed! 🐙 example (xs ys : List Nat) (p : List Nat → Prop) (h : p (xs ++ mk_symm ys).reverse) : p (mk_symm ys ++ xs.reverse) := xs:List Natys:List Natp:List Nat → Proph:p (xs ++ mk_symm ys).reverse⊢ p (mk_symm ys ++ xs.reverse) xs:List Natys:List Natp:List Nat → Proph:p (mk_symm ys ++ xs.reverse)⊢ p (mk_symm ys ++ xs.reverse); All goals completed! 🐙

이 속성은 정리가 선언된 이후 언제든지 적용할 수도 있습니다:

theorem reverse_mk_symm (xs : List α) : (mk_symm xs).reverse = mk_symm xs := α:Type u_1xs:List α⊢ (mk_symm xs).reverse = mk_symm xs All goals completed! 🐙 example (xs ys : List Nat) : (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse := xs:List Natys:List Nat⊢ (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse All goals completed! 🐙 attribute [simp] reverse_mk_symm example (xs ys : List Nat) (p : List Nat → Prop) (h : p (xs ++ mk_symm ys).reverse) : p (mk_symm ys ++ xs.reverse) := xs:List Natys:List Natp:List Nat → Proph:p (xs ++ mk_symm ys).reverse⊢ p (mk_symm ys ++ xs.reverse) xs:List Natys:List Natp:List Nat → Proph:p (mk_symm ys ++ xs.reverse)⊢ p (mk_symm ys ++ xs.reverse); All goals completed! 🐙

그러나 속성을 한번 적용하고 나면 이를 영구적으로 제거할 방법은 없습니다. 속성은 그것이 지정된 파일을 가져오는 모든 파일에서 계속 유지됩니다. 속성에서 더 자세히 다루겠지만, local 수식어를 사용하면 속성의 범위를 현재 파일이나 섹션으로 제한할 수 있습니다.

theorem reverse_mk_symm (xs : List α) : (mk_symm xs).reverse = mk_symm xs := α:Type u_1xs:List α⊢ (mk_symm xs).reverse = mk_symm xs All goals completed! 🐙 section attribute [local simp] reverse_mk_symm example (xs ys : List Nat) : (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse := xs:List Natys:List Nat⊢ (xs ++ mk_symm ys).reverse = mk_symm ys ++ xs.reverse All goals completed! 🐙 example (xs ys : List Nat) (p : List Nat → Prop) (h : p (xs ++ mk_symm ys).reverse) : p (mk_symm ys ++ xs.reverse) := xs:List Natys:List Natp:List Nat → Proph:p (xs ++ mk_symm ys).reverse⊢ p (mk_symm ys ++ xs.reverse) xs:List Natys:List Natp:List Nat → Proph:p (mk_symm ys ++ xs.reverse)⊢ p (mk_symm ys ++ xs.reverse); All goals completed! 🐙 end

섹션 밖에서는 단순화기가 더 이상 기본적으로 reverse_mk_symm을 사용하지 않습니다.

지금까지 살펴본 여러 simp 옵션, 즉 규칙 목록을 명시적으로 제공하는 것과 at을 사용하여 위치를 지정하는 것은 함께 사용할 수 있지만, 나열하는 순서는 엄격하게 정해져 있다는 점에 유의하십시오. 편집기에서 simp 식별자 위에 커서를 놓으면 이와 연결된 문서 문자열이 표시되므로, 이를 통해 올바른 순서를 확인할 수 있습니다.

유용한 수식어가 두 가지 더 있습니다. 기본적으로 simp는 [simp] 속성이 표시된 모든 정리를 사용합니다. simp only라고 쓰면 이러한 기본 정리들이 제외되므로, 더 명시적으로 구성한 규칙 목록을 사용할 수 있습니다. 아래 예제에서는 빼기 기호와 only를 사용하여 reverse_mk_symm이 적용되지 않도록 막습니다.

def mk_symm (xs : List α) := xs ++ xs.reverse @[simp] theorem reverse_mk_symm (xs : List α) : (mk_symm xs).reverse = mk_symm xs := α:Type u_1xs:List α⊢ (mk_symm xs).reverse = mk_symm xs All goals completed! 🐙 example (xs ys : List Nat) (p : List Nat → Prop) (h : p (xs ++ mk_symm ys).reverse) : p (mk_symm ys ++ xs.reverse) := xs:List Natys:List Natp:List Nat → Proph:p (xs ++ mk_symm ys).reverse⊢ p (mk_symm ys ++ xs.reverse) xs:List Natys:List Natp:List Nat → Proph:p (mk_symm ys ++ xs.reverse)⊢ p (mk_symm ys ++ xs.reverse); All goals completed! 🐙 example (xs ys : List Nat) (p : List Nat → Prop) (h : p (xs ++ mk_symm ys).reverse) : p ((mk_symm ys).reverse ++ xs.reverse) := xs:List Natys:List Natp:List Nat → Proph:p (xs ++ mk_symm ys).reverse⊢ p ((mk_symm ys).reverse ++ xs.reverse) xs:List Natys:List Natp:List Nat → Proph:p ((mk_symm ys).reverse ++ xs.reverse)⊢ p ((mk_symm ys).reverse ++ xs.reverse); All goals completed! 🐙 example (xs ys : List Nat) (p : List Nat → Prop) (h : p (xs ++ mk_symm ys).reverse) : p ((mk_symm ys).reverse ++ xs.reverse) := xs:List Natys:List Natp:List Nat → Proph:p (xs ++ mk_symm ys).reverse⊢ p ((mk_symm ys).reverse ++ xs.reverse) xs:List Natys:List Natp:List Nat → Proph:p ((mk_symm ys).reverse ++ xs.reverse)⊢ p ((mk_symm ys).reverse ++ xs.reverse); All goals completed! 🐙

simp 택틱에는 여러 가지 설정 옵션이 있습니다. 예를 들어, 다음과 같이 문맥적 단순화를 활성화할 수 있습니다.

example : if x = 0 then y + x = y else x ≠ 0 := x:Naty:Nat⊢ if x = 0 then y + x = y else x ≠ 0 All goals completed! 🐙

+contextual을 사용하면 simp 택틱은 y + x = y를 단순화할 때는 x = 0이라는 사실을, 다른 분기를 단순화할 때는 x ≠ 0이라는 사실을 사용합니다. 다음은 또 다른 예입니다:

example : ∀ (x : Nat) (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 : x = 0), y + x = y := y:Nat⊢ ∀ (x : Nat), x = 0 → y + x = y All goals completed! 🐙

또 하나의 유용한 설정 옵션으로는 산술적 단순화를 활성화하는 +arith가 있습니다.

example : 0 < 1 + x ∧ x + y + 2 ≥ y + 1 := x:Naty:Nat⊢ 0 < 1 + x ∧ x + y + 2 ≥ y + 1 All goals completed! 🐙

5.8. Split 택틱🔗

split 택틱은 중첩된 if-then-else 표현식과 match 표현식을 경우별로 나누는 데 유용합니다. n개의 경우를 가진 match 표현식에 대해 split 택틱은 최대 n개의 하위 목표를 생성합니다. 다음은 예시입니다:

def f (x y z : Nat) : Nat := match x, y, z with | 5, _, _ => y | _, 5, _ => y | _, _, 5 => y | _, _, _ => 1 example (x y z : Nat) : x ≠ 5 → y ≠ 5 → z ≠ 5 → z = w → f x y w = 1 := w:Natx:Naty:Natz:Nat⊢ x ≠ 5 → y ≠ 5 → z ≠ 5 → z = w → f x y w = 1 w:Natx:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5a✝:z = w⊢ f x y w = 1 w:Natx:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5a✝:z = w⊢ (match x, y, w with | 5, x, x_1 => y | x, 5, x_1 => y | x, x_1, 5 => y | x, x_1, x_2 => 1) = 1 w:Naty:Natz:Nata✝³:y ≠ 5a✝²:z ≠ 5a✝¹:z = wx✝:Naty✝:Natz✝:Nata✝:5 ≠ 5⊢ y = 1w:Natx:Natz:Nata✝³:x ≠ 5a✝²:z ≠ 5a✝¹:z = wx✝¹:Naty✝:Natz✝:Natx✝:x = 5 → Falsea✝:5 ≠ 5⊢ 5 = 1x:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5x✝²:Naty✝:Natz✝:Natx✝¹:x = 5 → Falsex✝:y = 5 → Falsea✝:z = 5⊢ y = 1w:Natx:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5a✝:z = wx✝³:Naty✝:Natz✝:Natx✝²:x = 5 → Falsex✝¹:y = 5 → Falsex✝:w = 5 → False⊢ 1 = 1 w:Naty:Natz:Nata✝³:y ≠ 5a✝²:z ≠ 5a✝¹:z = wx✝:Naty✝:Natz✝:Nata✝:5 ≠ 5⊢ y = 1 All goals completed! 🐙 w:Natx:Natz:Nata✝³:x ≠ 5a✝²:z ≠ 5a✝¹:z = wx✝¹:Naty✝:Natz✝:Natx✝:x = 5 → Falsea✝:5 ≠ 5⊢ 5 = 1 All goals completed! 🐙 x:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5x✝²:Naty✝:Natz✝:Natx✝¹:x = 5 → Falsex✝:y = 5 → Falsea✝:z = 5⊢ y = 1 All goals completed! 🐙 w:Natx:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5a✝:z = wx✝³:Naty✝:Natz✝:Natx✝²:x = 5 → Falsex✝¹:y = 5 → Falsex✝:w = 5 → False⊢ 1 = 1 All goals completed! 🐙

위의 택틱 증명은 다음과 같이 줄일 수 있습니다.

example (x y z : Nat) : x ≠ 5 → y ≠ 5 → z ≠ 5 → z = w → f x y w = 1 := w:Natx:Naty:Natz:Nat⊢ x ≠ 5 → y ≠ 5 → z ≠ 5 → z = w → f x y w = 1 w:Natx:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5a✝:z = w⊢ f x y w = 1; w:Natx:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5a✝:z = w⊢ (match x, y, w with | 5, x, x_1 => y | x, 5, x_1 => y | x, x_1, 5 => y | x, x_1, x_2 => 1) = 1; w:Naty:Natz:Nata✝³:y ≠ 5a✝²:z ≠ 5a✝¹:z = wx✝:Naty✝:Natz✝:Nata✝:5 ≠ 5⊢ y = 1w:Natx:Natz:Nata✝³:x ≠ 5a✝²:z ≠ 5a✝¹:z = wx✝¹:Naty✝:Natz✝:Natx✝:x = 5 → Falsea✝:5 ≠ 5⊢ 5 = 1x:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5x✝²:Naty✝:Natz✝:Natx✝¹:x = 5 → Falsex✝:y = 5 → Falsea✝:z = 5⊢ y = 1w:Natx:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5a✝:z = wx✝³:Naty✝:Natz✝:Natx✝²:x = 5 → Falsex✝¹:y = 5 → Falsex✝:w = 5 → False⊢ 1 = 1 w:Naty:Natz:Nata✝³:y ≠ 5a✝²:z ≠ 5a✝¹:z = wx✝:Naty✝:Natz✝:Nata✝:5 ≠ 5⊢ y = 1w:Natx:Natz:Nata✝³:x ≠ 5a✝²:z ≠ 5a✝¹:z = wx✝¹:Naty✝:Natz✝:Natx✝:x = 5 → Falsea✝:5 ≠ 5⊢ 5 = 1x:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5x✝²:Naty✝:Natz✝:Natx✝¹:x = 5 → Falsex✝:y = 5 → Falsea✝:z = 5⊢ y = 1w:Natx:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5a✝:z = wx✝³:Naty✝:Natz✝:Natx✝²:x = 5 → Falsex✝¹:y = 5 → Falsex✝:w = 5 → False⊢ 1 = 1 first | w:Natx:Naty:Natz:Nata✝³:x ≠ 5a✝²:y ≠ 5a✝¹:z ≠ 5a✝:z = wx✝³:Naty✝:Natz✝:Natx✝²:x = 5 → Falsex✝¹:y = 5 → Falsex✝:w = 5 → False⊢ 1 = 1 | All goals completed! 🐙

split <;> first | contradiction | rfl 택틱은 먼저 split 택틱을 적용한 다음, 생성된 각 목표에 대해 contradiction을 시도하고, contradiction이 실패하면 rfl을 시도합니다. simp와 마찬가지로 split도 특정 가설에 적용할 수 있습니다:

def g (xs ys : List Nat) : Nat := match xs, ys with | [a, b], _ => a+b+1 | _, [b, _] => b+1 | _, _ => 1 example (xs ys : List Nat) (h : g xs ys = 0) : False := xs:List Natys:List Nath:g xs ys = 0⊢ False xs:List Natys:List Nath:(match xs, ys with | [a, b], x => a + b + 1 | x, [b, head] => b + 1 | x, x_1 => 1) = 0⊢ False; ys:List Natxs✝:List Natys✝:List Nata✝:Natb✝:Nath:a✝ + b✝ + 1 = 0⊢ Falsexs:List Natxs✝:List Natys✝:List Natb✝:Nathead✝:Natx✝:∀ (a b : Nat), xs = [a, b] → Falseh:b✝ + 1 = 0⊢ Falsexs:List Natys:List Natxs✝:List Natys✝:List Natx✝¹:∀ (a b : Nat), xs = [a, b] → Falsex✝:∀ (b head : Nat), ys = [b, head] → Falseh:1 = 0⊢ False ys:List Natxs✝:List Natys✝:List Nata✝:Natb✝:Nath:a✝ + b✝ + 1 = 0⊢ Falsexs:List Natxs✝:List Natys✝:List Natb✝:Nathead✝:Natx✝:∀ (a b : Nat), xs = [a, b] → Falseh:b✝ + 1 = 0⊢ Falsexs:List Natys:List Natxs✝:List Natys✝:List Natx✝¹:∀ (a b : Nat), xs = [a, b] → Falsex✝:∀ (b head : Nat), ys = [b, head] → Falseh:1 = 0⊢ False All goals completed! 🐙

5.9. 확장 가능한 택틱🔗

다음 예제에서는 syntax 명령을 사용하여 triv 표기법을 정의합니다. 그런 다음 macro_rules 명령을 사용하여 triv가 사용될 때 무엇을 수행해야 하는지 지정합니다. 서로 다른 확장을 여러 개 제공할 수 있으며, 택틱 인터프리터는 그중 하나가 성공할 때까지 모두 시도합니다:

-- Define a new tactic notation syntax "triv" : tactic macro_rules | `(tactic| triv) => `(tactic| assumption) example (h : p) : p := p:Sort ?u.2h:p⊢ p All goals completed! 🐙 -- You cannot prove the following theorem using `triv` -- example (x : α) : x = x := by -- triv -- Let's extend `triv`. The tactic interpreter -- tries all possible macro extensions for `triv` until one succeeds macro_rules | `(tactic| triv) => `(tactic| rfl) example (x : α) : x = x := α:Sort u_1x:α⊢ x = x All goals completed! 🐙 example (x : α) (h : p) : x = x ∧ p := α:Sort u_1p:Propx:αh:p⊢ x = x ∧ p α:Sort u_1p:Propx:αh:p⊢ x = xα:Sort u_1p:Propx:αh:p⊢ p α:Sort u_1p:Propx:αh:p⊢ x = xα:Sort u_1p:Propx:αh:p⊢ p All goals completed! 🐙 -- We now add a (recursive) extension macro_rules | `(tactic| triv) => `(tactic| apply And.intro <;> triv) example (x : α) (h : p) : x = x ∧ p := α:Sort u_1p:Propx:αh:p⊢ x = x ∧ p All goals completed! 🐙

5.10. 연습 문제🔗

  1. 명제와 증명과 한정자와 상등의 연습 문제로 돌아가서, 이제 택틱 증명을 사용해 가능한 한 많이 다시 풀어 보십시오. 이때 적절하다면 rw와 simp도 사용하십시오.

  2. 택틱 조합자를 사용하여 다음 명제의 한 줄짜리 증명을 구하십시오:

declaration uses `sorry`example (p q r : Prop) (hp : p) : (p ∨ q ∨ r) ∧ (q ∨ p ∨ r) ∧ (q ∨ r ∨ p) := p:Propq:Propr:Prophp:p⊢ (p ∨ q ∨ r) ∧ (q ∨ p ∨ r) ∧ (q ∨ r ∨ p) All goals completed! 🐙