Lean 4로 정리 증명하기

8. 귀납법과 재귀🔗

이전 장에서 우리는 귀납적 정의가 Lean에서 새로운 타입을 도입하는 강력한 수단이 된다는 것을 살펴보았습니다. 더욱이, 생성자와 재귀자는 이러한 타입에 대한 함수를 정의하는 유일한 수단입니다. propositions-as-types 대응에 의해, 이는 귀납법이 증명의 근본적인 방법임을 의미합니다.

Lean은 재귀 함수를 정의하고, 패턴 매칭을 수행하고, 귀납적 증명을 작성하는 자연스러운 방법을 제공합니다. Lean을 사용하면 함수가 만족해야 할 방정식을 명시함으로써 함수를 정의할 수 있으며, 발생할 수 있는 다양한 경우를 어떻게 처리할지 명시함으로써 정리를 증명할 수 있습니다. 내부적으로는 이러한 서술이 “방정식 컴파일러”라고 부르는 절차를 통해 원시 재귀자로 “컴파일”됩니다. 방정식 컴파일러는 신뢰된 코드 기반의 일부가 아니며, 그 출력은 커널에 의해 독립적으로 검사되는 항들로 구성됩니다.

8.1. 패턴 매칭🔗

스키마 패턴의 해석은 컴파일 과정의 첫 단계입니다. 귀납적으로 정의된 타입에 관련된 생성자에 따라, casesOn 재귀자를 사용하여 함수를 정의하고 경우를 나누어 정리를 증명할 수 있음을 살펴보았습니다. 하지만 복잡한 정의는 여러 개의 중첩된 casesOn 적용을 사용할 수 있으며, 읽고 이해하기 어려울 수 있습니다. 패턴 매칭은 더 편리하고, 함수형 프로그래밍 언어 사용자에게 익숙한 접근 방식을 제공합니다.

귀납적으로 정의된 자연수 타입을 생각해 봅시다. 모든 자연수는 zero 이거나 succ x 이므로, 각 경우에 대한 값을 지정함으로써 자연수에서 임의의 타입으로 가는 함수를 정의할 수 있습니다.

open Nat def sub1 : Nat Nat | zero => zero | succ x => x def isZero : Nat Bool | zero => true | succ x => false

이 함수들을 정의하는 데 사용된 방정식들은 정의적으로 성립합니다:

example : sub1 0 = 0 := rfl example (x : Nat) : sub1 (succ x) = x := rfl example : isZero 0 = true := rfl example (x : Nat) : isZero (succ x) = false := rfl example : sub1 7 = 6 := rfl example (x : Nat) : isZero (x + 3) = false := rfl

zerosucc 대신, 더 익숙한 표기법을 사용할 수 있습니다:

def sub1 : Nat Nat | 0 => 0 | x + 1 => x def isZero : Nat Bool | 0 => true | x + 1 => false

덧셈과 0 표기법에는 [match_pattern] 속성이 부여되어 있으므로, 이들은 패턴 매칭에 사용될 수 있습니다. Lean은 생성자 zerosucc가 드러날 때까지 이러한 표현식을 단순히 정규화합니다.

패턴 매칭은 곱 타입이나 option 타입과 같이 모든 귀납적 타입에서 작동합니다:

def swap : α × β β × α | (a, b) => (b, a) def foo : Nat × Nat Nat | (m, n) => m + n def bar : Option Nat Nat | some n => n + 1 | none => 0

여기서는 이를 함수를 정의하는 데 사용할 뿐만 아니라, 경우를 나누는 증명을 수행하는 데에도 사용합니다:

def not : Bool Bool | true => false | false => true theorem not_not : (b : Bool), not (not b) = b | true => show not (not true) = true from rfl | false => show not (not false) = false from rfl

패턴 매칭은 귀납적으로 정의된 명제를 해체하는 데에도 사용할 수 있습니다:

example (p q : Prop) : p q q p | And.intro h₁ h₂ => And.intro h₂ h₁ example (p q : Prop) : p q q p | Or.inl hp => Or.inr hp | Or.inr hq => Or.inl hq

이는 논리 결합자를 사용하는 가정을 간결하게 풀어내는 방법을 제공합니다.

이 모든 예시에서, 패턴 매칭은 단일한 경우 구분을 수행하는 데 사용되었습니다. 더 흥미로운 점은, 다음 예시들에서처럼 패턴이 중첩된 생성자를 포함할 수 있다는 것입니다.

def sub2 : Nat Nat | 0 => 0 | 1 => 0 | x + 2 => x

방정식 컴파일러는 먼저 입력이 zero인지 succ x 형태인지에 따라 경우를 나눕니다. 그런 다음 xzero 형태인지 succ x 형태인지에 따라 경우 분할을 수행합니다. 컴파일러는 제시된 패턴들로부터 필요한 경우 분할을 결정하며, 패턴이 모든 경우를 다 포괄하지 못하면 오류를 발생시킵니다. 이번에도 아래 버전에서처럼 산술 표기법을 사용할 수 있습니다. 두 경우 모두 정의 방정식은 정의적으로 성립합니다.

example : sub2 0 = 0 := rfl example : sub2 1 = 0 := rfl example : sub2 (x+2) = x := rfl example : sub2 5 = 3 := rfl

def sub2 : Nat Nat := fun x => match x with | 0 => 0 | 1 => 0 | x.succ.succ => x#print sub2를 작성하면 이 함수가 어떻게 재귀자로 컴파일되었는지 확인할 수 있습니다. (Lean은 sub2가 내부 보조 함수인 sub2.match_1을 통해 정의되었다고 알려주는데, 이 함수 역시 출력해 볼 수 있습니다.) Lean은 이러한 보조 함수를 사용하여 match 표현식을 컴파일합니다. 사실 위의 정의는 다음과 같이 확장됩니다

def sub2 : Nat Nat := fun x => match x with | 0 => 0 | 1 => 0 | x + 2 => x

다음은 중첩된 패턴 매칭의 몇 가지 추가 예시입니다:

example (p q : α Prop) : ( x, p x q x) ( x, p x) ( x, q x) | Exists.intro x (Or.inl px) => Or.inl (Exists.intro x px) | Exists.intro x (Or.inr qx) => Or.inr (Exists.intro x qx) def foo : Nat × Nat Nat | (0, n) => 0 | (m+1, 0) => 1 | (m+1, n+1) => 2

방정식 컴파일러는 여러 인자를 순차적으로 처리할 수 있습니다. 예를 들어, 이전 예제를 두 개의 인자를 받는 함수로 정의하는 것이 더 자연스러울 것입니다.

def foo : Nat Nat Nat | 0, n => 0 | m + 1, 0 => 1 | m + 1, n + 1 => 2

또 다른 예를 살펴보겠습니다:

def bar : List Nat List Nat Nat | [], [] => 0 | a :: as, [] => a | [], b :: bs => b | a :: as, b :: bs => a + b

패턴은 쉼표로 구분된다는 점에 유의하십시오.

다음 각 예제에서, 나머지 인자들도 패턴 목록에 포함되어 있지만, 분할은 첫 번째 인자에 대해서만 일어납니다.

def and : Bool Bool Bool | true, a => a | false, _ => false def or : Bool Bool Bool | true, _ => true | false, a => a def cond : Bool α α α | true, x, y => x | false, x, y => y

또한 정의에서 인자의 값이 필요하지 않을 때는 밑줄을 대신 사용할 수 있다는 점에 유의하십시오. 이 밑줄은 와일드카드 패턴, 또는 익명 변수라고 알려져 있습니다. 방정식 컴파일러 밖에서의 사용과 달리, 여기서 밑줄은 암시적 인자를 나타내지 않습니다. 와일드카드를 위해 밑줄을 사용하는 것은 함수형 프로그래밍 언어에서 흔한 일이며, Lean도 그 표기법을 채택하고 있습니다. 와일드카드와 겹치는 패턴에 관한 절에서는 와일드카드의 개념을 더 자세히 다루며, 접근할 수 없는 패턴에 대한 설명에서는 패턴에서도 암시적 인자를 사용하는 방법을 설명합니다.

귀납적 타입에서 설명한 대로, 귀납적 데이터 타입은 매개변수에 의존할 수 있습니다. 다음 예제는 패턴 매칭을 사용하여 tail 함수를 정의합니다. 인자 α : Type u는 매개변수이며, 패턴 매칭에 참여하지 않음을 나타내기 위해 콜론 앞에 위치합니다. Lean은 매개변수가 : 뒤에 오는 것도 허용하지만, 그 경우 패턴 매칭을 하려면 명시적인 match가 필요합니다.

def tail1 {α : Type u} : List α List α | [] => [] | a :: as => as def tail2 : {α : Type u} List α List α | α, [] => [] | α, a :: as => as

이 두 예제에서 매개변수 α의 배치는 다르지만, 두 경우 모두 케이스 분할에 관여하지 않는다는 점에서 동일한 방식으로 처리됩니다.

Lean은 더 복잡한 형태의 패턴 매칭도 처리할 수 있는데, 이러한 경우에는 의존 타입의 인자가 각 경우에 추가적인 제약을 가합니다. 이러한 의존 패턴 매칭의 예시는 의존 패턴 매칭 절에서 다룹니다.

8.2. 와일드카드와 중복 패턴🔗

이전 절의 예제 중 하나를 살펴봅시다:

def foo : Nat Nat Nat | 0, n => 0 | m + 1, 0 => 1 | m + 1, n + 1 => 2

대안적인 표현은 다음과 같습니다:

def foo : Nat Nat Nat | 0, n => 0 | m, 0 => 1 | m, n => 2

두 번째 표현에서는 패턴이 겹칩니다. 예를 들어 인자 쌍 0, 0은 세 경우 모두와 매칭됩니다. 하지만 Lean은 적용 가능한 첫 번째 방정식을 사용하여 이 모호함을 처리하므로, 이 예제에서 최종 결과는 동일합니다. 특히 다음 방정식들은 정의적으로 성립합니다:

example : foo 0 0 = 0 := rfl example : foo 0 (n + 1) = 0 := rfl example : foo (m + 1) 0 = 1 := rfl example : foo (m + 1) (n + 1) = 2 := rfl

mn의 값이 필요하지 않으므로, 와일드카드 패턴을 대신 사용해도 무방합니다.

def foo : Nat Nat Nat | 0, _ => 0 | _, 0 => 1 | _, _ => 2

foo의 이 정의가 이전과 동일한 정의적 항등성을 만족한다는 것을 확인할 수 있습니다.

일부 함수형 프로그래밍 언어는 불완전한 패턴을 지원합니다. 이러한 언어에서는 인터프리터가 불완전한 경우에 대해 예외를 발생시키거나 임의의 값을 반환합니다. Inhabited 타입 클래스를 사용하여 임의의 값을 반환하는 방식을 흉내 낼 수 있습니다. 대략적으로 말하면, Inhabited α의 원소는 α에 원소가 존재한다는 사실의 증거입니다. 타입 클래스에 관한 장에서 보게 되겠지만, Lean에게 적절한 기본 타입에 원소가 있음을 알려줄 수 있고, 다른 구성된 타입에 원소가 있음을 자동으로 추론하게 할 수도 있습니다. 이를 바탕으로, 표준 라이브러리는 원소가 있는 모든 타입에 대해 기본 원소인 default를 제공합니다.

Option α 타입을 사용하여 불완전한 패턴을 시뮬레이션할 수도 있습니다. 아이디어는 제공된 패턴에 대해서는 some a를 반환하고, 불완전한 경우에는 none을 사용하는 것입니다. 다음 예제는 두 방식을 모두 보여 줍니다.

def f1 : Nat Nat Nat | 0, _ => 1 | _, 0 => 2 | _, _ => default -- the "incomplete" case example : f1 0 0 = 1 := rfl example : f1 0 (a+1) = 1 := rfl example : f1 (a+1) 0 = 2 := rfl example : f1 (a+1) (b+1) = default := rfl def f2 : Nat Nat Option Nat | 0, _ => some 1 | _, 0 => some 2 | _, _ => none -- the "incomplete" case example : f2 0 0 = some 1 := rfl example : f2 0 (a+1) = some 1 := rfl example : f2 (a+1) 0 = some 2 := rfl example : f2 (a+1) (b+1) = none := rfl

이퀘이션 컴파일러는 영리합니다. 다음 정의에서 케이스 중 하나라도 빠뜨리면, 오류 메시지가 무엇이 다루어지지 않았는지 알려줄 것입니다.

def bar : Nat List Nat Bool Nat | 0, _, false => 0 | 0, b :: _, _ => b | 0, [], true => 7 | a+1, [], false => a | a+1, [], true => a + 1 | a+1, b :: _, _ => a + b

또한 적절한 상황에서는 casesOn 대신 if ... then ... else를 사용합니다.

def foo : Char Nat | 'A' => 1 | 'B' => 2 | _ => 3 @[instance_reducible] def foo.match_1.{u_1} : (motive : Char Sort u_1) (x : Char) (Unit motive 'A') (Unit motive 'B') ((x : Char) motive x) motive x := fun motive x h_1 h_2 h_3 => dite (x = 'A') (Eq.ndrec_symm (h_1 ())) fun h_1 => dite (x = 'B') (Eq.ndrec_symm (h_2 ())) fun h_2 => h_3 x#print foo.match_1
@[instance_reducible] def foo.match_1.{u_1} : (motive : Char  Sort u_1) 
  (x : Char)  (Unit  motive 'A')  (Unit  motive 'B')  ((x : Char)  motive x)  motive x :=
fun motive x h_1 h_2 h_3 =>
  dite (x = 'A') (Eq.ndrec_symm (h_1 ())) fun h_1 => dite (x = 'B') (Eq.ndrec_symm (h_2 ())) fun h_2 => h_3 x

8.3. 구조적 재귀와 귀납법🔗

방정식 컴파일러를 강력하게 만드는 것은 재귀적 정의도 지원한다는 점입니다. 다음 세 절에서는 각각 다음을 설명합니다:

  • 구조적 재귀 정의

  • 정초된(well-founded) 재귀적 정의

  • 상호 재귀적 정의

일반적으로 방정식 컴파일러는 다음과 같은 형태의 입력을 처리합니다.

def foo (a : α) : (b : β) → γ
  | [patterns₁] => t₁
  ...
  | [patternsₙ] => tₙ

여기서 (a : α)는 매개변수들의 나열이고, (b : β)는 패턴 매칭이 이루어지는 인자들의 나열이며, γab에 의존할 수 있는 임의의 타입입니다. 각 줄은 β의 각 원소마다 하나씩, 동일한 개수의 패턴을 포함해야 합니다. 지금까지 살펴보았듯이, 패턴은 변수이거나, 다른 패턴들에 적용된 생성자이거나, 혹은 그러한 형태로 정규화되는 식(이때 생성자가 아닌 것은 [match_pattern] 속성으로 표시됩니다)입니다. 생성자의 출현은 경우 분리를 유발하며, 생성자에 대한 인자는 주어진 변수로 표현됩니다. 의존 패턴 매칭에 관한 절에서, 패턴 매칭에서 아무런 역할을 하지 않음에도 불구하고 식이 타입 검사를 통과하도록 만들기 위해 패턴 안의 일부 명시적 항이 특정한 형태로 강제되는 것을 보게 될 것입니다. 바로 이런 이유로 이러한 것들을 “접근 불가능한 패턴”이라고 부릅니다. 하지만 의존 패턴 매칭을 다루기 전까지는 이러한 접근 불가능한 패턴을 사용할 필요가 없을 것입니다.

지난 절에서 살펴보았듯이, 항 t₁, ..., tₙ은 매개변수 a 중 어느 것이든, 그리고 해당 패턴에서 도입된 변수 중 어느 것이든 사용할 수 있습니다. 재귀와 귀납법을 가능하게 하는 것은 이들이 foo에 대한 재귀 호출도 포함할 수 있다는 점입니다. 이 절에서는 구조적 재귀를 다룰 것인데, 이는 =>의 오른쪽에 나타나는 foo의 인자들이 왼쪽 패턴들의 부분항인 경우입니다. 그 발상은 이들이 구조적으로 더 작으므로 귀납적 타입에서 더 이른 단계에 나타난다는 것입니다. 다음은 지난 장에서 다룬 구조적 재귀의 예시들로, 이제 등식 컴파일러를 사용하여 정의된 것입니다:

open Nat def add : Nat Nat Nat | m, zero => m | m, succ n => succ (add m n) theorem add_zero (m : Nat) : add m zero = m := rfl theorem add_succ (m n : Nat) : add m (succ n) = succ (add m n) := rfl theorem zero_add : n, add zero n = n | zero => rfl | succ n => congrArg succ (zero_add n) def mul : Nat Nat Nat | Variable name `n` 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] _n Note: This linter can be disabled with `set_option linter.unusedVariables false`n, zero => zero | n, succ m => add (mul n m) n

zero_add의 증명을 보면 귀납법에 의한 증명이 실제로는 Lean에서 재귀의 한 형태임이 분명해집니다.

위의 예시는 add의 정의 방정식이 정의적으로 성립함을 보여주며, mul도 마찬가지입니다. 방정식 컴파일러는 단순한 구조적 귀납법의 경우처럼, 가능한 한 이것이 성립하도록 보장하고자 합니다. 그러나 다른 상황에서는 축소가 오직 명제적으로만 성립하는데, 이는 곧 명시적으로 적용해야 하는 방정식 정리라는 뜻입니다. 방정식 컴파일러는 이러한 정리를 내부적으로 생성합니다. 이는 사용자가 직접 사용하도록 의도된 것이 아니며, 대신 simp 택틱이 필요할 때 이를 사용하도록 설정되어 있습니다. zero_add의 다음 증명이 이러한 방식으로 작동합니다.

theorem zero_add : n, add zero n = n add zero zero = zero add zero zero = zero All goals completed! 🐙 n:Natadd zero n.succ = n.succ n:Natadd zero n.succ = n.succ All goals completed! 🐙

패턴 매칭에 의한 정의와 마찬가지로, 구조적 재귀나 귀납법에 대한 매개변수는 콜론 앞에 나타날 수 있습니다. 이러한 매개변수는 정의가 처리되기 전에 단순히 지역 컨텍스트에 추가됩니다. 예를 들어, 덧셈의 정의는 다음과 같이 작성될 수도 있습니다.

open Nat def add (m : Nat) : Nat Nat | zero => m | succ n => succ (add m n)

위의 예제는 match를 사용해서 작성할 수도 있습니다.

open Nat def add (m n : Nat) : Nat := match n with | zero => m | succ n => succ (add m n)

구조적 재귀의 더 흥미로운 예시는 피보나치 함수 fib에서 볼 수 있습니다.

def fib : Nat Nat | 0 => 1 | 1 => 1 | n+2 => fib (n+1) + fib n example : fib 0 = 1 := rfl example : fib 1 = 1 := rfl example : fib (n + 2) = fib (n + 1) + fib n := rfl example : fib 7 = 21 := rfl

여기서 n + 2에서의 fib 함수 값(이는 succ (succ n)과 정의적으로 같습니다)은 n + 1에서의 값(이는 succ n과 정의적으로 동등합니다)과 n에서의 값을 이용하여 정의됩니다. 그러나 이는 피보나치 함수를 계산하는 데 있어 악명 높게 비효율적인 방법으로, 실행 시간이 n에 대해 지수적으로 증가합니다. 더 나은 방법은 다음과 같습니다:

def fibFast (n : Nat) : Nat := (loop n).2 where loop : Nat Nat × Nat | 0 => (0, 1) | n+1 => let p := loop n; (p.2, p.1 + p.2) 573147844013817084101#eval fibFast 100
573147844013817084101

다음은 where 대신 let rec을 사용한 동일한 정의입니다.

def fibFast (n : Nat) : Nat := let rec loop : Nat Nat × Nat | 0 => (0, 1) | n+1 => let p := loop n; (p.2, p.1 + p.2) (loop n).2

두 경우 모두, Lean은 보조 함수 fibFast.loop를 생성합니다.

구조적 재귀를 처리하기 위해, 방정식 컴파일러는 각 귀납적으로 정의된 타입마다 자동으로 생성되는 상수 belowbrecOn을 사용하여 course-of-values 재귀를 사용합니다. Nat.belowNat.brecOn의 타입을 살펴보면 이것이 어떻게 작동하는지 감을 잡을 수 있습니다:

variable (C : Nat Type u) Nat.below : Nat Type u#check (@Nat.below C : Nat Type u)
Nat.below : Nat  Type u
Nat.below 3#reduce @Nat.below C (3 : Nat)
Nat.below 3
Nat.brecOn : (t : Nat) ((t : Nat) Nat.below t C t) C t#check (@Nat.brecOn C : (n : Nat) ((n : Nat) @Nat.below C n C n) C n)
Nat.brecOn : (t : Nat)  ((t : Nat)  Nat.below t  C t)  C t

@Nat.below C (3 : Nat) 타입은 C 0, C 1, C 2의 원소들을 저장하는 데이터 구조입니다. 코스-오브-밸류 재귀(course-of-values recursion)는 Nat.brecOn에 의해 구현됩니다. 이를 통해 (n : Nat) C n 타입의 의존 함수의 값을 특정 입력 n에 대해, @Nat.below C n의 원소로 표현되는 함수의 모든 이전 값들을 이용해 정의할 수 있습니다.

코스-오브-밸류즈 재귀(course-of-values recursion)의 사용은 방정식 컴파일러가 Lean 커널에게 함수가 종료됨을 정당화하기 위해 사용하는 기법 중 하나입니다. 이는 다른 함수형 프로그래밍 언어 컴파일러와 마찬가지로 재귀 함수를 컴파일하는 코드 생성기에는 영향을 미치지 않습니다. #eval fib <n><n>에 대해 지수적임을 상기하십시오. 반면, #reduce fib <n>brecOn 구성에 기반하여 커널로 전송된 정의를 사용하기 때문에 효율적입니다.

def fib : Nat Nat | 0 => 1 | 1 => 1 | n+2 => fib (n+1) + fib n -- Slow: -- #eval fib 50 -- Fast: 20365011074#reduce fib 50
20365011074
def fib : Nat Nat := fun x => Nat.brecOn x fib._f#print fib
def fib : Nat  Nat :=
fun x => Nat.brecOn x fib._f

재귀적 정의의 또 다른 좋은 예시는 리스트 append 함수입니다.

def append : List α List α List α | [], bs => bs | a::as, bs => a :: append as bs example : append [1, 2, 3] [4, 5] = [1, 2, 3, 4, 5] := rfl

또 다른 예시가 있습니다: 이 함수는 두 목록 중 하나가 소진될 때까지 첫 번째 목록의 원소와 두 번째 목록의 원소를 더합니다.

def listAdd [Add α] : List α List α List α | [], _ => [] | _, [] => [] | a :: as, b :: bs => (a + b) :: listAdd as bs [5, 7, 9]#eval listAdd [1, 2, 3] [4, 5, 6, 6, 9, 10]
[5, 7, 9]

아래 연습 문제에서 유사한 예제들을 직접 실험해 보시기를 권장합니다.

8.4. 지역 재귀 선언🔗

let rec 키워드를 사용하여 지역 재귀적 선언을 정의할 수 있습니다.

def replicate (n : Nat) (a : α) : List α := let rec loop : Nat List α List α | 0, as => as | n+1, as => loop n (a::as) loop n [] @replicate.loop : {α : Type u_1} α Nat List α List α#check @replicate.loop
@replicate.loop : {α : Type u_1}  α  Nat  List α  List α

Lean은 각 let rec에 대해 보조 선언을 생성합니다. 위 예제에서는 replicate에서 나타나는 let rec loop에 대해 replicate.loop 선언을 생성했습니다. 참고로, Lean은 let rec 선언 안에 나타나는 지역 변수를 추가 매개변수로 덧붙여 해당 선언을 “닫습니다”. 예를 들어, 지역 변수 alet rec loop에서 나타납니다.

택틱 모드와 귀납법을 통한 증명을 작성할 때도 let rec을 사용할 수 있습니다.

theorem length_replicate (n : Nat) (a : α) : (replicate n a).length = n := α:Type u_1n:Nata:α(replicate n a).length = n let rec aux (n : Nat) (as : List α) : (replicate.loop a n as).length = n + as.length := α:Type u_1n✝:Nata:αn:Natas:List α(replicate.loop a n as).length = n + as.length match n with α:Type u_1n✝:Nata:αn:Natas:List α(replicate.loop a 0 as).length = 0 + as.length All goals completed! 🐙 α:Type u_1n✝¹:Nata:αn✝:Natas:List αn:Nat(replicate.loop a (n + 1) as).length = n + 1 + as.length All goals completed! 🐙 All goals completed! 🐙

정의 뒤에 where 절을 사용하여 보조 재귀 선언을 도입할 수도 있습니다. Lean은 이를 let rec으로 변환합니다.

def replicate (n : Nat) (a : α) : List α := loop n [] where loop : Nat List α List α | 0, as => as | n+1, as => loop n (a::as) theorem length_replicate (n : Nat) (a : α) : (replicate n a).length = n := α:Type u_1n:Nata:α(replicate n a).length = n All goals completed! 🐙 where aux (n : Nat) (as : List α) : (replicate.loop a n as).length = n + as.length := α:Type u_1n✝:Nata:αn:Natas:List α(replicate.loop a n as).length = n + as.length match n with α:Type u_1n✝:Nata:αn:Natas:List α(replicate.loop a 0 as).length = 0 + as.length All goals completed! 🐙 α:Type u_1n✝¹:Nata:αn✝:Natas:List αn:Nat(replicate.loop a (n + 1) as).length = n + 1 + as.length All goals completed! 🐙

8.5. 정초 재귀법과 귀납법🔗

구조적 재귀법을 사용할 수 없는 경우, 정지성(termination)을 정초 재귀법(well-founded recursion)을 사용하여 증명할 수 있습니다. 정초 관계(well-founded relation)와 각 재귀 호출이 이 관계에 대해 감소함을 보이는 증명이 필요합니다. 의존 타입 이론은 정초 재귀법을 인코딩하고 정당화하기에 충분히 강력합니다. 정초 재귀법이 어떻게 작동하는지 이해하는 데 필요한 논리적 배경부터 시작합시다.

Lean의 표준 라이브러리는 두 가지 술어인 Acc r aWellFounded r을 정의하는데, 여기서 r은 타입 α에 대한 이항 관계이고, a는 타입 α의 원소입니다.

variable (α : Sort u) variable (r : α α Prop) Acc r : α Prop#check (Acc r : α Prop)
Acc r : α  Prop
WellFounded r : Prop#check (WellFounded r : Prop)
WellFounded r : Prop

첫 번째인 Acc는 귀납적으로 정의된 술어입니다. 그 정의에 따르면, Acc r x y, r y x Acc r y와 동치입니다. r y x를 일종의 순서 관계 y x를 나타내는 것으로 생각한다면, Acc r x는 모든 선행자가 접근 가능하다는 의미에서 x가 아래로부터 접근 가능하다는 것을 말합니다. 특히, x에 선행 원소가 없다면 그것은 접근 가능합니다. 임의의 타입 α가 주어졌을 때, 우리는 모든 선행 원소에 먼저 값을 할당함으로써 재귀적으로 α의 각 접근 가능한 원소에 값을 할당할 수 있어야 합니다.

r이 정초적(well-founded)이라는 명제, 즉 WellFounded r로 표기되는 명제는, 정확히 해당 타입의 모든 원소가 접근 가능하다는 명제입니다. 위의 고찰에 따르면, r이 타입 α에 대한 정초적 관계라면, 관계 r에 대해 α에 대한 정초적 재귀의 원리가 존재해야 합니다. 그리고 실제로 그러합니다: 표준 라이브러리는 정확히 그러한 목적을 위해 WellFounded.fix를 정의합니다.

noncomputable def f {α : Sort u} (r : α α Prop) (h : WellFounded r) (C : α Sort v) (F : (x : α) ((y : α) r y x C y) C x) : (x : α) C x := WellFounded.fix h F

여기에는 등장하는 요소가 많지만, 첫 번째 묶음은 이미 살펴본 바 있습니다: 타입 α, 관계 r, 그리고 r이 정초적임을 나타내는 가정 h입니다. 변수 C는 재귀적 정의의 동기를 나타냅니다: 각 원소 x : α에 대해, C x의 원소를 구성하고자 합니다. 함수 F는 이를 위한 귀납적 방법을 제공합니다: x의 각 선행 원소 y에 대한 C y의 원소들이 주어졌을 때, C x의 원소를 어떻게 구성하는지 알려줍니다.

WellFounded.fix가 귀납법 원리로서도 동일하게 작동한다는 점에 유의하십시오. 이는 가 정초적(well-founded)이고 x, C x를 증명하고자 할 때, 임의의 x에 대해 y, r y x C y가 성립한다고 가정하면 C x가 성립함을 보이는 것으로 충분하다는 것을 의미합니다.

위 예제에서 noncomputable 수식어를 사용한 이유는 코드 생성기가 현재 WellFounded.fix를 지원하지 않기 때문입니다. WellFounded.fix 함수는 Lean이 함수의 종료를 정당화하는 데 사용하는 또 다른 도구입니다.

Lean은 자연수에 대한 일반적인 순서 <가 정초적(well founded)임을 알고 있습니다. 또한 Lean은 사전식 순서를 사용하는 것과 같이, 기존의 정초적 순서로부터 새로운 정초적 순서를 구성하는 여러 방법도 알고 있습니다.

다음은 표준 라이브러리에서 찾을 수 있는 자연수 나눗셈 정의를 본질적으로 담은 것입니다.

open Nat theorem div_lemma {x y : Nat} : 0 < y y x x - y < x := fun h => sub_lt (Nat.lt_of_lt_of_le h.left h.right) h.left def div.F (x : Nat) (f : (x₁ : Nat) x₁ < x Nat Nat) (y : Nat) : Nat := if h : 0 < y y x then f (x - y) (div_lemma h) y + 1 else zero noncomputable def div := WellFounded.fix (measure id).wf div.F

이 정의는 다소 이해하기 어렵습니다. 여기서 재귀는 x에 대해 이루어지며, div.F x f : Nat → Nat는 그 고정된 x에 대해 “y로 나누기” 함수를 반환합니다. div.F의 두 번째 인자, 즉 재귀를 위한 레시피는 x보다 작은 모든 값 x₁에 대해 y로 나누기 함수를 반환해야 하는 함수라는 점을 기억해야 합니다.

읽기 어렵다는 점 외에도, div에 대한 이 정의는 div 8 24와 정의적으로 같아지도록 이끌지 않습니다.

example : div 8 2 = 4 := div 8 2 = 4 Tactic `rfl` failed: The left-hand side div 8 2 is not definitionally equal to the right-hand side 4 div 8 2 = 4div 8 2 = 4
Tactic `rfl` failed: The left-hand side
  div 8 2
is not definitionally equal to the right-hand side
  4

div 8 2 = 4

대신, 이 방정식은 명제적으로만 성립하며, 접근 가능한 관계에 관한 표준 라이브러리 정리를 사용하여 증명해야 합니다:

example : div 8 2 = 4 := div 8 2 = 4 All goals completed! 🐙

정교화기는 이와 같은 정의를 더 편리하게 만들도록 설계되었습니다. 다음을 받아들입니다.

def div (x y : Nat) : Nat := if h : 0 < y y x then have : x - y < x := Nat.sub_lt (Nat.lt_of_lt_of_le h.1 h.2) h.1 div (x - y) y + 1 else 0

Lean이 재귀적 정의를 만나면, 먼저 구조적 재귀를 시도하고, 그것이 실패할 때만 정초 재귀(well-founded recursion)로 대체합니다. Lean은 재귀적 적용이 더 작다는 것을 보이기 위해 택틱 decreasing_tactic을 사용합니다. 위 예제의 보조 명제 x - y < x는 이 택틱을 위한 힌트로 간주되어야 합니다. 정교화기는 또한 재귀적 정의에 대한 일련의 등식 보조정리들을 증명하여, 접근 가능한 관계에 대한 저수준 보조정리에 의존하지 않고도 이를 다룰 수 있게 합니다.

div에 대한 정의 방정식은 정의적으로 성립하지 않지만, unfold 택틱을 사용하여 div를 펼칠 수 있습니다. 어떤 div 적용을 펼칠지 선택하기 위해 conv를 사용합니다.

example (x y : Nat) : div x y = if 0 < y y x then div (x - y) y + 1 else 0 := x:Naty:Natdiv x y = if 0 < y y x then div (x - y) y + 1 else 0 -- unfold occurrence in the left-hand-side of the equation: x:Naty:Nat| div x y = if 0 < y y x then div (x - y) y + 1 else 0 x:Naty:Nat| div x y; x:Naty:Nat| if h : 0 < y y x then have this := ; div (x - y) y + 1 else 0 All goals completed! 🐙 example (x y : Nat) (h : 0 < y y x) : div x y = div (x - y) y + 1 := x:Naty:Nath:0 < y y xdiv x y = div (x - y) y + 1 x:Naty:Nath:0 < y y x| div x y = div (x - y) y + 1 x:Naty:Nath:0 < y y x| div x y; x:Naty:Nath:0 < y y x| if h : 0 < y y x then have this := ; div (x - y) y + 1 else 0 All goals completed! 🐙

다음 예제도 이와 비슷합니다. 이 예제는 임의의 자연수를 0과 1의 리스트로 표현되는 이진 표현으로 변환합니다. 재귀 호출이 감소한다는 증거를 제공해야 하는데, 여기서는 sorry로 이를 처리합니다. sorry가 있다고 해서 인터프리터가 함수를 성공적으로 평가하지 못하는 것은 아니지만, 항에 sorry가 포함된 경우에는 #eval 대신 #eval!을 사용해야 합니다.

def declaration uses `sorry`declaration uses `sorry`declaration uses `sorry`natToBin : Nat List Nat | 0 => [0] | 1 => [1] | n + 2 => have : (n + 2) / 2 < n + 2 := sorry natToBin ((n + 2) / 2) ++ [n % 2] [1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1]#eval! natToBin 1234567

마지막 예시로, 자연수에 대한 사전식 순서의 정초성(well-foundedness)에 의해 정당화되므로 애커만 함수를 직접 정의할 수 있음을 관찰합니다. termination_by 절은 Lean에게 사전식 순서를 사용하도록 지시합니다. 이 절은 실제로 함수 인자들을 Nat × Nat 타입의 원소로 매핑합니다. 그런 다음 Lean은 타입 클래스 해결을 사용하여 WellFoundedRelation (Nat × Nat) 타입의 원소를 합성합니다.

def ack : Nat Nat Nat | 0, y => y+1 | x+1, 0 => ack x 1 | x+1, y+1 => ack x (ack (x+1) y) termination_by x y => (x, y)

많은 경우, Lean은 적절한 사전식 순서를 자동으로 결정할 수 있습니다. 애커만 함수는 그러한 경우 중 하나이므로, termination_by 절은 선택 사항입니다.

def ack : Nat Nat Nat | 0, y => y+1 | x+1, 0 => ack x 1 | x+1, y+1 => ack x (ack (x+1) y)

위 예제에서 사전식 순서가 사용된 이유는 인스턴스 WellFoundedRelation (α × β)가 사전식 순서를 사용하기 때문이라는 점에 유의하십시오. Lean은 또한 다음 인스턴스도 정의합니다.

instance (priority := low) [SizeOf α] : WellFoundedRelation α := sizeOfWFRel

다음 예제에서는 재귀 적용에서 as.size - i가 감소함을 보임으로써 종료를 증명합니다.

def takeWhile (p : α Bool) (as : Array α) : Array α := go 0 #[] where go (i : Nat) (r : Array α) : Array α := if h : i < as.size then let a := as[i] if p a then go (i+1) (r.push a) else r else r termination_by as.size - i

이 예제에서 보조 함수 go는 재귀적이지만, takeWhile은 그렇지 않다는 점에 유의하십시오. 다시 한번, Lean은 이 패턴을 자동으로 인식할 수 있으므로 termination_by 절이 불필요합니다.

def takeWhile (p : α Bool) (as : Array α) : Array α := go 0 #[] where go (i : Nat) (r : Array α) : Array α := if h : i < as.size then let a := as[i] if p a then go (i+1) (r.push a) else r else r

기본적으로 Lean은 재귀 호출이 감소함을 증명하기 위해 decreasing_tactic 택틱을 사용합니다. decreasing_by 수정자를 사용하면 우리만의 택틱을 제공할 수 있습니다. 다음은 그 예시입니다.

theorem div_lemma {x y : Nat} : 0 < y y x x - y < x := fun ypos, ylex => Nat.sub_lt (Nat.lt_of_lt_of_le ypos ylex) ypos def div (x y : Nat) : Nat := if h : 0 < y y x then div (x - y) y + 1 else 0 decreasing_by y:Natx:Nath:0 < y y x0 < y y x; All goals completed! 🐙

decreasing_bytermination_by의 대체물이 아니며, 서로를 보완한다는 점에 유의하십시오. termination_by는 정초 관계(well-founded relation)를 지정하는 데 사용되고, decreasing_by는 재귀 호출이 감소함을 보이기 위한 우리만의 택틱을 제공하는 데 사용됩니다. 다음 예제에서는 이 둘을 함께 사용합니다.

def ack : Nat Nat Nat | 0, y => y+1 | x+1, 0 => ack x 1 | x+1, y+1 => ack x (ack (x+1) y) termination_by x y => (x, y) decreasing_by -- unfolds well-founded recursion auxiliary definitions: all_goals x:Naty:Natx✝:(y_1 : (_ : Nat) ×' Nat) (invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 y_1 x.succ, y.succ NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (x, x✝ x + 1, y ) (x + 1, y + 1) x:NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (x, 1) (x + 1, 0) x:Natx < x + 1; All goals completed! 🐙 x:Naty:NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (x + 1, y) (x + 1, y + 1) x:Naty:Naty < y + 1; All goals completed! 🐙 x:Naty:Natx✝:(y_1 : (_ : Nat) ×' Nat) (invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 y_1 x.succ, y.succ NatProd.Lex (fun a₁ a₂ => a₁ < a₂) (fun a₁ a₂ => a₁ < a₂) (x, x✝ x + 1, y ) (x + 1, y + 1) x:Naty:Natx✝:(y_1 : (_ : Nat) ×' Nat) (invImage (fun x => PSigma.casesOn x fun a a_1 => (a, a_1)) Prod.instWellFoundedRelation).1 y_1 x.succ, y.succ Natx < x + 1; All goals completed! 🐙

decreasing_by sorry를 사용하여 함수가 종료됨을 “믿어” 달라고 Lean에 지시할 수 있습니다.

declaration uses `sorry`def declaration uses `sorry`natToBin : Nat List Nat | 0 => [0] | 1 => [1] | n + 2 => natToBin ((n + 2) / 2) ++ [n % 2] decreasing_by All goals completed! 🐙 [1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1]#eval! natToBin 1234567

sorry를 사용하는 것은 새로운 공리를 사용하는 것과 동등하며, 피해야 한다는 점을 기억하십시오. 다음 예제에서는 False를 증명하기 위해 sorry를 사용했습니다. #print axioms unsound 명령은 unsoundsorry를 구현하는 데 사용된 건전하지 않은 공리 sorryAx에 의존함을 보여줍니다.

Definition `unsound` is a proposition; use `theorem` instead of `def` Note: This linter can be disabled with `set_option linter.defProp false`def declaration uses `sorry`unsound (x : Nat) : False := unsound (x + 1) decreasing_by All goals completed! 🐙 unsound 0 : False#check unsound 0
unsound 0 : False
-- `unsound 0` is a proof of `False` 'unsound' depends on axioms: [sorryAx]#print axioms unsound
'unsound' depends on axioms: [sorryAx]

요약:

  • termination_by가 없는 경우, 인자를 하나 선택한 다음 타입 클래스 해석을 사용하여 해당 인자의 타입에 대한 정초 관계(well-founded relation)를 합성함으로써 (가능하다면) 정초 관계가 도출됩니다.

  • termination_by가 지정되면, 이는 함수의 인자를 타입 α로 매핑하며 타입 클래스 해소가 다시 사용됩니다. β × γ에 대한 기본 인스턴스는 βγ의 정초 관계(well-founded relation)에 기반한 사전식 순서라는 점을 상기하십시오.

  • Nat에 대한 기본 정초 관계(well-founded relation) 인스턴스는 (· < ·)입니다.

  • 기본적으로, 재귀적 적용이 선택된 정초 관계(well-founded relation)에 대해 더 작음을 보이기 위해 decreasing_tactic 택틱이 사용됩니다. decreasing_tactic이 실패하면, 오류 메시지에 남은 목표 ... |- G가 포함됩니다. decreasing_tacticassumption을 사용한다는 점에 유의하십시오. 따라서, 목표 G를 증명하기 위해 have-표현식을 포함시킬 수 있습니다. decreasing_by를 사용해 직접 택틱을 제공할 수도 있습니다.

8.6. 함수적 귀납법🔗

Lean은 재귀 함수를 위한 맞춤형 귀납법 원리를 생성합니다. 이러한 귀납법 원리는 데이터 타입의 구조가 아니라 함수 정의의 재귀 구조를 따릅니다. 함수에 대한 증명은 일반적으로 함수 자체의 재귀 구조를 따르므로, 이러한 귀납법 원리를 사용하면 함수에 대한 명제를 더 편리하게 증명할 수 있습니다.

예를 들어, ack에 대한 함수적 귀납법 원리를 사용하여 결과가 항상 0보다 크다는 것을 증명하려면 ack의 패턴 매칭에 있는 각 분기마다 하나씩의 경우가 필요합니다:

def ack : Nat Nat Nat | 0, y => y+1 | x+1, 0 => ack x 1 | x+1, y+1 => ack x (ack (x+1) y) theorem ack_gt_zero : ack n m > 0 := n:Natm:Natack n m > 0 fun_induction ack with y:Naty + 1 > 0 All goals completed! 🐙 x:Natih:ack x 1 > 0ack x 1 > 0 All goals completed! 🐙 x:Naty:Natih1:ack (x + 1) y > 0ih2:ack x (ack (x + 1) y) > 0ack x (ack (x + 1) y) > 0 All goals completed! 🐙

case1y:Naty + 1 > 0에서 목표는 다음과 같습니다:

y:Naty + 1 > 0

목표에 있는 y + 1ack의 첫 번째 경우에서 반환되는 값에 해당합니다.

case2x:Natih:ack x 1 > 0ack x 1 > 0에서 목표는 다음과 같습니다:

x:Natih:ack x 1 > 0ack x 1 > 0

목표에 있는 ack x 1은 패턴 변수 x + 10에 적용된 ack의 값에 대응하며, 이는 ack의 두 번째 경우에서 반환됩니다. 이 항은 자동으로 우변으로 단순화됩니다. 다행히도 귀납 가정 ih : ack x 1 > 0은 재귀 호출에 대응하며, 이는 바로 이 경우에서 반환되는 답과 정확히 일치합니다.

case3x:Naty:Natih1:ack (x + 1) y > 0ih2:ack x (ack (x + 1) y) > 0ack x (ack (x + 1) y) > 0에서 목표는 다음과 같습니다:

x:Naty:Natih1:ack (x + 1) y > 0ih2:ack x (ack (x + 1) y) > 0ack x (ack (x + 1) y) > 0

목표에 있는 ack x (ack (x + 1) y)ackx + 1y + 1에 적용하여 축약했을 때, ack의 세 번째 경우에서 반환되는 값에 해당합니다. 귀납 가설 ih1 : ack (x + 1) y > 0ih2 : ack x (ack (x + 1) y) > 0은 재귀 호출에 해당하며, ih1은 중첩된 재귀 호출과 일치합니다. 다시 한 번, 귀납법 가설이 적합합니다.

fun_induction ack를 사용하면 ack의 재귀 구조와 일치하는 목표와 귀납법 가설이 만들어집니다. 그 결과, 증명은 한 줄로 작성할 수 있습니다:

theorem ack_gt_zero : ack n m > 0 := n:Natm:Natack n m > 0 y✝:Naty✝ + 1 > 0x✝:Natih1✝:ack x✝ 1 > 0ack x✝ 1 > 0x✝:Naty✝:Natih2✝:ack (x✝ + 1) y✝ > 0ih1✝:ack x✝ (ack (x✝ + 1) y✝) > 0ack x✝ (ack (x✝ + 1) y✝) > 0 y✝:Naty✝ + 1 > 0x✝:Natih1✝:ack x✝ 1 > 0ack x✝ 1 > 0x✝:Naty✝:Natih2✝:ack (x✝ + 1) y✝ > 0ih1✝:ack x✝ (ack (x✝ + 1) y✝) > 0ack x✝ (ack (x✝ + 1) y✝) > 0 All goals completed! 🐙

cases 택틱과 유사한 fun_cases 택틱도 있습니다. 이 택틱은 함수의 제어 흐름에서 각 분기마다 케이스를 생성합니다. 이 택틱과 fun_induction은 추가로 선택되지 않은 경로를 배제하는 가정을 제공합니다.

이 함수 f는 다섯 갈래 불리언 논리합을 나타냅니다:

def f : Bool Bool Bool Bool Bool Bool | true, _, _, _ , _ => true | _, true, _, _ , _ => true | _, _, true, _ , _ => true | _, _, _, true, _ => true | _, _, _, _, x => x

그것이 논리합임을 증명하려면, 마지막 경우는 인자 중 어느 것도 true가 아니라는 지식이 필요합니다. 이 지식은 다음 택틱에 의해 제공됩니다.

theorem declaration uses `sorry`f_or : f b1 b2 b3 b4 b5 = (b1 || b2 || b3 || b4 || b5) := b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolf b1 b2 b3 b4 b5 = (b1 || b2 || b3 || b4 || b5) b2:Boolb3:Boolb4:Boolb5:Booltrue = (true || b2 || b3 || b4 || b5)b1:Boolb3:Boolb4:Boolb5:Boolx✝:b1 = true Falsetrue = (b1 || true || b3 || b4 || b5)b1:Boolb2:Boolb4:Boolb5:Boolx✝¹:b1 = true Falsex✝:b2 = true Falsetrue = (b1 || b2 || true || b4 || b5)b1:Boolb2:Boolb3:Boolb5:Boolx✝²:b1 = true Falsex✝¹:b2 = true Falsex✝:b3 = true Falsetrue = (b1 || b2 || b3 || true || b5)b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolx✝³:b1 = true Falsex✝²:b2 = true Falsex✝¹:b3 = true Falsex✝:b4 = true Falseb5 = (b1 || b2 || b3 || b4 || b5) all_goals All goals completed! 🐙

각 경우에는 이전 경우들을 배제하는 가정이 포함됩니다:

b2:Boolb3:Boolb4:Boolb5:Booltrue = (true || b2 || b3 || b4 || b5)b1:Boolb3:Boolb4:Boolb5:Boolx✝:b1 = true Falsetrue = (b1 || true || b3 || b4 || b5)b1:Boolb2:Boolb4:Boolb5:Boolx✝¹:b1 = true Falsex✝:b2 = true Falsetrue = (b1 || b2 || true || b4 || b5)b1:Boolb2:Boolb3:Boolb5:Boolx✝²:b1 = true Falsex✝¹:b2 = true Falsex✝:b3 = true Falsetrue = (b1 || b2 || b3 || true || b5)b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolx✝³:b1 = true Falsex✝²:b2 = true Falsex✝¹:b3 = true Falsex✝:b4 = true Falseb5 = (b1 || b2 || b3 || b4 || b5)

모든 가정과 목표를 함께 단순화하는 simp_all 택틱은 모든 경우를 처리할 수 있습니다:

theorem f_or : f b1 b2 b3 b4 b5 = (b1 || b2 || b3 || b4 || b5) := b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolf b1 b2 b3 b4 b5 = (b1 || b2 || b3 || b4 || b5) b2:Boolb3:Boolb4:Boolb5:Booltrue = (true || b2 || b3 || b4 || b5)b1:Boolb3:Boolb4:Boolb5:Boolx✝:b1 = true Falsetrue = (b1 || true || b3 || b4 || b5)b1:Boolb2:Boolb4:Boolb5:Boolx✝¹:b1 = true Falsex✝:b2 = true Falsetrue = (b1 || b2 || true || b4 || b5)b1:Boolb2:Boolb3:Boolb5:Boolx✝²:b1 = true Falsex✝¹:b2 = true Falsex✝:b3 = true Falsetrue = (b1 || b2 || b3 || true || b5)b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolx✝³:b1 = true Falsex✝²:b2 = true Falsex✝¹:b3 = true Falsex✝:b4 = true Falseb5 = (b1 || b2 || b3 || b4 || b5) b2:Boolb3:Boolb4:Boolb5:Booltrue = (true || b2 || b3 || b4 || b5)b1:Boolb3:Boolb4:Boolb5:Boolx✝:b1 = true Falsetrue = (b1 || true || b3 || b4 || b5)b1:Boolb2:Boolb4:Boolb5:Boolx✝¹:b1 = true Falsex✝:b2 = true Falsetrue = (b1 || b2 || true || b4 || b5)b1:Boolb2:Boolb3:Boolb5:Boolx✝²:b1 = true Falsex✝¹:b2 = true Falsex✝:b3 = true Falsetrue = (b1 || b2 || b3 || true || b5)b1:Boolb2:Boolb3:Boolb4:Boolb5:Boolx✝³:b1 = true Falsex✝²:b2 = true Falsex✝¹:b3 = true Falsex✝:b4 = true Falseb5 = (b1 || b2 || b3 || b4 || b5) All goals completed! 🐙

8.7. 상호 재귀🔗

Lean은 상호 재귀 정의도 지원합니다. 그 문법은 상호 귀납적 타입의 문법과 유사합니다. 다음은 예제입니다.

mutual def even : Nat Bool | 0 => true | n+1 => odd n def odd : Nat Bool | 0 => false | n+1 => even n end example : even (a + 1) = odd a := a:Nateven (a + 1) = odd a All goals completed! 🐙 example : odd (a + 1) = even a := a:Natodd (a + 1) = even a All goals completed! 🐙 theorem even_eq_not_odd : a, even a = not (odd a) := (a : Nat), even a = !odd a a:Nateven a = !odd a; even 0 = !odd 0n✝:Nata✝:even n✝ = !odd n✝even (n✝ + 1) = !odd (n✝ + 1) even 0 = !odd 0 All goals completed! 🐙 n✝:Nata✝:even n✝ = !odd n✝even (n✝ + 1) = !odd (n✝ + 1) All goals completed! 🐙

이것이 상호 정의가 되는 이유는 evenodd를 이용해 재귀적으로 정의되는 동시에, oddeven을 이용해 재귀적으로 정의되기 때문입니다. 내부적으로 이는 단일한 재귀 정의로 컴파일됩니다. 내부적으로 정의된 함수는 인자로 합 타입의 원소를 받는데, 이는 even에 대한 입력이거나 odd에 대한 입력입니다. 그런 다음 입력에 알맞은 출력을 반환합니다. 이 함수를 정의하기 위해, Lean은 적절한 정초 측도(well-founded measure)를 사용합니다. 내부 구현은 사용자에게 감추어지도록 되어 있습니다. 위에서 했던 것처럼, 이러한 정의를 활용하는 정형적인 방법은 simp(또는 unfold)를 사용하는 것입니다.

상호 재귀 정의는 상호적이고 중첩된 귀납적 타입을 다루는 자연스러운 방법도 제공합니다. 앞서 제시했던 것처럼 상호 귀납적 술어로서 EvenOdd의 정의를 상기해 보십시오.

mutual inductive Even : Nat Prop where | even_zero : Even 0 | even_succ : n, Odd n Even (n + 1) inductive Odd : Nat Prop where | odd_succ : n, Even n Odd (n + 1) end

생성자 even_zero, even_succ, odd_succ는 어떤 수가 짝수인지 홀수인지를 보이는 긍정적인 수단을 제공합니다. 0이 홀수가 아니라는 것과 뒤의 두 함의가 역방향으로도 성립한다는 것을 알기 위해서는, 이 귀납적 타입이 이러한 생성자들에 의해 생성된다는 사실을 이용해야 합니다. 늘 그렇듯이 생성자들은 정의 대상 타입의 이름을 딴 네임스페이스 안에 보관되며, open Even Odd 명령을 사용하면 이들에 더 편리하게 접근할 수 있습니다.

open Even Odd theorem not_odd_zero : ¬ Odd 0 := fun h => nomatch h theorem even_of_odd_succ : n, Odd (n + 1) Even n | _, odd_succ n h => h theorem odd_of_even_succ : n, Even (n + 1) Odd n | _, even_succ n h => h

또 다른 예로, 중첩 귀납적 타입을 사용해 항(term)의 집합을 귀납적으로 정의한다고 가정합시다. 이때 항은 (문자열로 이름이 주어지는) 상수이거나, 상수를 상수들의 목록에 적용한 결과입니다.

inductive Term where | const : String Term | app : String List Term Term

그러면 상호 재귀적 정의를 사용하여 항에 나타나는 상수의 개수와 항의 목록에 나타나는 개수를 셀 수 있습니다.

namespace Term mutual def numConsts : Term Nat | const _ => 1 | app _ cs => numConstsLst cs def numConstsLst : List Term Nat | [] => 0 | c :: cs => numConsts c + numConstsLst cs end def sample := app "f" [app "g" [const "x"], const "y"] 2#eval numConsts sample
2
end Term

마지막 예제로, 항 e에서 상수 ab로 치환하는 함수 replaceConst a b e를 정의한 다음, 상수의 개수가 동일함을 증명합니다. 이 증명은 상호 재귀(귀납법이라고도 함)를 사용한다는 점에 유의하십시오.

mutual def replaceConst (a b : String) : Term Term | const c => if a == c then const b else const c | app f cs => app f (replaceConstLst a b cs) def replaceConstLst (a b : String) : List Term List Term | [] => [] | c :: cs => replaceConst a b c :: replaceConstLst a b cs end mutual theorem numConsts_replaceConst (a b : String) (e : Term) : numConsts (replaceConst a b e) = numConsts e := a:Stringb:Stringe:Term(replaceConst a b e).numConsts = e.numConsts match e with a:Stringb:Stringe:Termc:String(replaceConst a b (const c)).numConsts = (const c).numConsts a:Stringb:Stringe:Termc:String(if a = c then const b else const c).numConsts = (const c).numConsts; a:Stringb:Stringe:Termc:Stringh✝:a = c(const b).numConsts = (const c).numConstsa:Stringb:Stringe:Termc:Stringh✝:¬a = c(const c).numConsts = (const c).numConsts a:Stringb:Stringe:Termc:Stringh✝:a = c(const b).numConsts = (const c).numConstsa:Stringb:Stringe:Termc:Stringh✝:¬a = c(const c).numConsts = (const c).numConsts All goals completed! 🐙 a:Stringb:Stringe:Termf:Stringcs:List Term(replaceConst a b (app f cs)).numConsts = (app f cs).numConsts All goals completed! 🐙 theorem numConsts_replaceConstLst (a b : String) (es : List Term) : numConstsLst (replaceConstLst a b es) = numConstsLst es := a:Stringb:Stringes:List TermnumConstsLst (replaceConstLst a b es) = numConstsLst es match es with a:Stringb:Stringes:List TermnumConstsLst (replaceConstLst a b []) = numConstsLst [] All goals completed! 🐙 a:Stringb:Stringes:List Termc:Termcs:List TermnumConstsLst (replaceConstLst a b (c :: cs)) = numConstsLst (c :: cs) All goals completed! 🐙 end

8.8. 의존 패턴 매칭🔗

패턴 매칭에 관한 절에서 살펴본 패턴 매칭의 모든 예시는 casesOnrecOn을 사용하여 쉽게 작성할 수 있습니다. 하지만 경우 나누기가 인덱스 값에 제약을 부과하기 때문에, Vect α n과 같은 인덱스가 있는 귀납적 패밀리의 경우 종종 그렇지 않습니다. 방정식 컴파일러가 없다면, 재귀자를 사용하여 map, zip, unzip과 같은 매우 간단한 함수를 정의하는 데도 상당량의 상용구 코드가 필요할 것입니다. 이 어려움을 이해하기 위해, 벡터 v : Vect α (n + 1)를 받아 첫 번째 원소를 삭제하는 함수 tail을 정의하는 데 무엇이 필요할지 생각해 보십시오.

inductive Vect (α : Type u) : Nat Type u | nil : Vect α 0 | cons : α {n : Nat} Vect α n Vect α (n + 1)

처음 떠오르는 생각은 Vect.casesOn 함수를 사용하는 것일 수 있습니다:

Vect.casesOn.{u, v} {α : Type v} {motive : (a : Nat) Vect α a Sort u} {a : Nat} (t : Vect α a) (nil : motive 0 nil) (cons : (a : α) {n : Nat} (a_1 : Vect α n) motive (n + 1) (cons a a_1)) : motive a t

하지만 nil 경우에는 어떤 값을 반환해야 할까요? 뭔가 이상한 일이 벌어지고 있습니다: vVect α (n + 1) 타입을 가진다면 이는 nil가 없지만, 이를 Vect.casesOn에 어떻게 알려야 할지는 분명하지 않습니다.

한 가지 해결책은 보조 함수를 정의하는 것입니다:

def tailAux (v : Vect α m) : m = n + 1 Vect α n := Vect.casesOn (motive := fun x _ => x = n + 1 Vect α n) v (fun h : 0 = n + 1 => Nat.noConfusion h) (fun (a : α) (m : Nat) (as : Vect α m) => fun (h : m + 1 = n + 1) => Nat.noConfusion h (fun h1 : m = n => h1 as)) def tail (v : Vect α (n+1)) : Vect α n := tailAux v rfl

nil 경우에는 m0으로 인스턴스화되며, Nat.noConfusion0 = n + 1이 발생할 수 없다는 사실을 이용합니다. 그렇지 않은 경우, vcons a as 형태이며, 이를 길이 m의 벡터에서 길이 n의 벡터로 캐스팅한 다음, 단순히 as를 반환하면 됩니다.

tail을 정의할 때의 어려움은 인덱스 간의 관계를 유지하는 데 있습니다. tailAux에 있는 m = n + 1이라는 가정은 n과 부전제(minor premise)에 연관된 인덱스 사이의 관계를 전달하는 데 사용됩니다. 게다가, 0 = n + 1 경우는 도달 불가능하며, 이러한 경우를 폐기하는 정형화된 방법은 Nat.noConfusion을 사용하는 것입니다.

그러나 tail 함수는 재귀 방정식을 사용하여 정의하기 쉬우며, 방정식 컴파일러가 모든 상용구 코드를 자동으로 생성해 줍니다. 다음은 유사한 예제 몇 가지입니다:

def head : {n : Nat} Vect α (n+1) α | n, cons a as => a def tail : {n : Nat} Vect α (n+1) Vect α n | n, cons a as => as theorem eta : {n : Nat} (v : Vect α (n+1)), cons (head v) (tail v) = v | n, cons a as => rfl def map (f : α β γ) : {n : Nat} Vect α n Vect β n Vect γ n | 0, nil, nil => nil | n+1, cons a as, cons b bs => cons (f a b) (map f as bs) def zip : {n : Nat} Vect α n Vect β n Vect (α × β) n | 0, nil, nil => nil | n+1, cons a as, cons b bs => cons (a, b) (zip as bs)

head nil과 같은 “도달 불가능한” 경우에 대해서는 재귀 방정식을 생략할 수 있다는 점에 유의하십시오. 색인 계열(indexed family)에 대해 자동으로 생성되는 정의는 결코 단순하지 않습니다. 예를 들면:

def zipWith (f : α β γ) : {n : Nat} Vect α n Vect β n Vect γ n | 0, nil, nil => nil | n+1, cons a as, cons b bs => cons (f a b) (zipWith f as bs) def Vect.zipWith.{u_1, u_2, u_3} : {α : Type u_1} {β : Type u_2} {γ : Type u_3} (α β γ) {n : Nat} Vect α n Vect β n Vect γ n := fun {α} {β} {γ} f x x_1 x_2 => Vect.brecOn (motive := fun x x_3 => Vect β x Vect γ x) x_1 (zipWith._f f) x_2#print zipWith
def Vect.zipWith.{u_1, u_2, u_3} : {α : Type u_1} 
  {β : Type u_2}  {γ : Type u_3}  (α  β  γ)  {n : Nat}  Vect α n  Vect β n  Vect γ n :=
fun {α} {β} {γ} f x x_1 x_2 => Vect.brecOn (motive := fun x x_3 => Vect β x  Vect γ x) x_1 (zipWith._f f) x_2
@[instance_reducible] def Vect.zipWith.match_1.{u_1, u_2, u_3} : {α : Type u_1} {β : Type u_2} (motive : (x : Nat) Vect α x Vect β x Sort u_3) (x : Nat) (x_1 : Vect α x) (x_2 : Vect β x) (Unit motive 0 nil nil) ((n : Nat) (a : α) (as : Vect α n) (b : β) (bs : Vect β n) motive n.succ (cons a as) (cons b bs)) motive x x_1 x_2 := fun {α} {β} motive x x_1 x_2 h_1 h_2 => Nat.casesOn (motive := fun x => (x_3 : Vect α x) (x_4 : Vect β x) motive x x_3 x_4) x (fun x x_3 => casesOn (motive := fun a x_4 => Nat.zero = a x x_4 motive Nat.zero x x_3) x (fun h h_3 => casesOn (motive := fun a x => Nat.zero = a x_3 x motive Nat.zero nil x_3) x_3 (fun h h_4 => h_1 ()) (fun a {n} a_1 h => False.elim ) ) (fun a {n} a_1 h => False.elim ) ) (fun n x x_3 => casesOn (motive := fun a x_4 => n.succ = a x x_4 motive n.succ x x_3) x (fun h => False.elim ) (fun a {n_1} a_1 h => Nat.Internal.elimOffset n n_1 1 h fun x_4 => Eq.ndrec (motive := fun {n_2} => (a_2 : Vect α n_2) x cons a a_2 motive n.succ x x_3) (fun a_2 h => casesOn (motive := fun a_3 x => n.succ = a_3 x_3 x motive n.succ (cons a a_2) x_3) x_3 (fun h => False.elim ) (fun a_3 {n_2} a_4 h => Nat.Internal.elimOffset n n_2 1 h fun x => Eq.ndrec (motive := fun {n_3} => (a_5 : Vect β n_3) x_3 cons a_3 a_5 motive n.succ (cons a a_2) x_3) (fun a_5 h => h_2 n a a_2 a_3 a_5) x a_4) ) x_4 a_1) ) x_1 x_2#print zipWith.match_1
@[instance_reducible] def Vect.zipWith.match_1.{u_1, u_2, u_3} : {α : Type u_1} 
  {β : Type u_2} 
    (motive : (x : Nat)  Vect α x  Vect β x  Sort u_3) 
      (x : Nat) 
        (x_1 : Vect α x) 
          (x_2 : Vect β x) 
            (Unit  motive 0 nil nil) 
              ((n : Nat) 
                  (a : α)  (as : Vect α n)  (b : β)  (bs : Vect β n)  motive n.succ (cons a as) (cons b bs)) 
                motive x x_1 x_2 :=
fun {α} {β} motive x x_1 x_2 h_1 h_2 =>
  Nat.casesOn (motive := fun x => (x_3 : Vect α x)  (x_4 : Vect β x)  motive x x_3 x_4) x
    (fun x x_3 =>
      casesOn (motive := fun a x_4 => Nat.zero = a  x  x_4  motive Nat.zero x x_3) x
        (fun h h_3 =>
           
            casesOn (motive := fun a x => Nat.zero = a  x_3  x  motive Nat.zero nil x_3) x_3
              (fun h h_4 =>   h_1 ()) (fun a {n} a_1 h => False.elim )  )
        (fun a {n} a_1 h => False.elim )  )
    (fun n x x_3 =>
      casesOn (motive := fun a x_4 => n.succ = a  x  x_4  motive n.succ x x_3) x (fun h => False.elim )
        (fun a {n_1} a_1 h =>
          Nat.Internal.elimOffset n n_1 1 h fun x_4 =>
            Eq.ndrec (motive := fun {n_2} => (a_2 : Vect α n_2)  x  cons a a_2  motive n.succ x x_3)
              (fun a_2 h =>
                 
                  casesOn (motive := fun a_3 x => n.succ = a_3  x_3  x  motive n.succ (cons a a_2) x_3) x_3
                    (fun h => False.elim )
                    (fun a_3 {n_2} a_4 h =>
                      Nat.Internal.elimOffset n n_2 1 h fun x =>
                        Eq.ndrec (motive := fun {n_3} =>
                          (a_5 : Vect β n_3)  x_3  cons a_3 a_5  motive n.succ (cons a a_2) x_3)
                          (fun a_5 h =>   h_2 n a a_2 a_3 a_5) x a_4)
                     )
              x_4 a_1)
         )
    x_1 x_2

zipWith 함수는 tail 함수보다도 손으로 정의하기가 훨씬 더 번거롭습니다. Vect.recOn, Vect.casesOn, Vect.noConfusion을 사용하여 직접 시도해 보시기를 권장합니다.

8.9. 접근 불가능한 패턴🔗

때때로 의존적인 매칭 패턴 내의 인자는 정의에 필수적이지는 않지만, 그럼에도 표현식의 타입을 적절히 특수화하기 위해 포함되어야 합니다. Lean은 사용자가 패턴 매칭을 위해 이러한 하위 항을 접근 불가능한(inaccessible) 것으로 표시할 수 있게 해줍니다. 이러한 표기는, 예를 들어 좌변에 나타나는 항이 변수도 아니고 생성자 적용도 아닌 경우 필수적인데, 이는 이러한 항들이 패턴 매칭의 적절한 대상이 아니기 때문입니다. 이러한 접근 불가능한 패턴은 패턴의 “신경 쓰지 않는(don't care)” 구성 요소로 볼 수 있습니다. 하위 항을 접근 불가능하다고 선언하려면 .(t)를 작성하면 됩니다. 접근 불가능한 패턴을 추론할 수 있는 경우에는 _를 작성할 수도 있습니다.

다음 예제에서는 “f의 상에 속함”이라는 속성을 정의하는 귀납적 타입을 선언합니다. 타입 ImageOf f b의 원소는 bf의 상에 속한다는 증거로 볼 수 있으며, 이때 생성자 imf는 그러한 증거를 구성하는 데 사용됩니다. 그런 다음 f의 상에 속하는 것을 그것으로 사상되는 원소로 보내는 “역함수”를 가진 임의의 함수 f를 정의할 수 있습니다. 타이핑 규칙 때문에 첫 번째 인자로 f a를 작성할 수밖에 없지만, 이 항은 변수도 생성자 적용도 아니며 패턴 매칭 정의에서 아무 역할도 하지 않습니다. 아래에서 함수 inverse를 정의하려면 f a를 접근 불가능(inaccessible)으로 반드시 표시해야 합니다.

inductive ImageOf {α β : Type u} (f : α β) : β Type u where | imf : (a : α) ImageOf f (f a) open ImageOf def inverse {f : α β} : (b : β) ImageOf f b α | .(f a), imf a => a def inverse' {f : α β} : (b : β) ImageOf f b α | _, imf a => a

위 예제에서 접근 불가능 주석은 f가 패턴 매칭 변수가 아님을 명확히 해줍니다.

접근 불가능한 패턴은 의존적 패턴 매칭을 사용하는 정의를 명확히 하고 제어하는 데 사용할 수 있습니다. 어떤 타입에 연관된 덧셈 함수가 있다고 가정하고, 그 타입의 원소로 이루어진 두 벡터를 더하는 함수 Vect.add의 다음 정의를 살펴보겠습니다.

inductive Vect (α : Type u) : Nat Type u | nil : Vect α 0 | cons : α {n : Nat} Vect α n Vect α (n+1) def Vect.add [Add α] : {n : Nat} Vect α n Vect α n Vect α n | 0, nil, nil => nil | Variable name `n` 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] _n Note: This linter can be disabled with `set_option linter.unusedVariables false`n+1, cons a as, cons b bs => cons (a + b) (add as bs)

인수 {n : Nat}는 정의 전체에 걸쳐 고정된 채로 유지될 수 없기 때문에 콜론 뒤에 나타납니다. 이 정의를 구현할 때, 방정식 컴파일러는 첫 번째 인수가 0인지 아니면 n+1 형태인지에 대한 경우 구분으로 시작합니다. 그 뒤를 이어 다음 두 인수에 대한 중첩된 경우 분할이 이어지며, 각 경우마다 방정식 컴파일러는 첫 번째 패턴과 양립할 수 없는 경우들을 배제합니다.

하지만 실제로는 첫 번째 인자에 대한 케이스 분할이 필요하지 않습니다. Vect에 대한 casesOn 제거자는 두 번째 인자에 대해 케이스 분할을 수행할 때 이 인자를 자동으로 추상화하여 0n + 1로 대체합니다. 접근 불가능한 패턴을 사용하면 방정식 컴파일러가 n에 대한 케이스 분할을 피하도록 유도할 수 있습니다.

def add [Add α] : {n : Nat} Vect α n Vect α n Vect α n | .(_), nil, nil => nil | .(_), cons a as, cons b bs => cons (a + b) (add as bs)

위치를 접근 불가능한 패턴으로 표시하는 것은 방정식 컴파일러에게, 첫째로 해당 인자의 형태가 다른 인자들이 부과하는 제약으로부터 추론되어야 한다는 것을, 둘째로 첫 번째 인자는 패턴 매칭에 참여하지 않아야 한다는 것을 알려줍니다.

접근 불가능한 패턴 .(_)는 편의를 위해 _로 쓸 수 있습니다.

def add [Add α] : {n : Nat} Vect α n Vect α n Vect α n | _, nil, nil => nil | _, cons a as, cons b bs => cons (a + b) (add as bs)

위에서 언급했듯이, 인자 {n : Nat}는 정의 전체에 걸쳐 고정될 수 없기 때문에 패턴 매칭의 일부입니다. 이러한 판별자들을 명시적으로 제공하도록 요구하는 대신, Lean은 암묵적으로 이 추가 판별자들을 자동으로 포함시켜 줍니다.

def add [Add α] {n : Nat} : Vect α n Vect α n Vect α n | nil, nil => nil | cons a as, cons b bs => cons (a + b) (add as bs)

auto bound implicits 기능과 결합하면 선언을 더욱 단순화하여 다음과 같이 작성할 수 있습니다.

def add [Add α] : Vect α n Vect α n Vect α n | nil, nil => nil | cons a as, cons b bs => cons (a + b) (add as bs)

이 새로운 기능들을 사용하면 이전 절에서 정의한 다른 벡터 함수들을 다음과 같이 더 간결하게 작성할 수 있습니다.

def head : Vect α (n+1) α | cons a as => a def tail : Vect α (n+1) Vect α n | cons a as => as theorem eta : (v : Vect α (n+1)) cons (head v) (tail v) = v | cons a as => rfl def map (f : α β γ) : Vect α n Vect β n Vect γ n | nil, nil => nil | cons a as, cons b bs => cons (f a b) (map f as bs) def zip : Vect α n Vect β n Vect (α × β) n | nil, nil => nil | cons a as, cons b bs => cons (a, b) (zip as bs)

8.10. 매치 표현식🔗

Lean은 또한 많은 함수형 언어에서 볼 수 있는 match-with 표현식을 위한 컴파일러도 제공합니다:

def isNotZero (m : Nat) : Bool := match m with | 0 => false | n + 1 => true

이는 일반적인 패턴 매칭 정의와 크게 달라 보이지 않지만, 핵심은 match가 표현식 어디에서나, 그리고 임의의 인자와 함께 사용될 수 있다는 점입니다.

def isNotZero (m : Nat) : Bool := match m with | 0 => false | n + 1 => true def filter (p : α Bool) : List α List α | [] => [] | a :: as => match p a with | true => a :: filter p as | false => filter p as example : filter isNotZero [1, 0, 0, 3, 0] = [1, 3] := rfl

또 다른 예시입니다:

def foo (n : Nat) (b c : Bool) := 5 + match n - 5, b && c with | 0, true => 0 | m + 1, true => m + 7 | 0, false => 5 | m + 1, false => m + 3 9#eval foo 7 true false
9
example : foo 7 true false = 9 := rfl

Lean은 시스템의 모든 부분에서 패턴 매칭을 구현하기 위해 내부적으로 match 구문을 사용합니다. 따라서 이 네 가지 정의는 모두 동일한 최종 효과를 가집니다.

def bar₁ : Nat × Nat Nat | (m, n) => m + n def bar₂ (p : Nat × Nat) : Nat := match p with | (m, n) => m + n def bar₃ : Nat × Nat Nat := fun (m, n) => m + n def bar₄ (p : Nat × Nat) : Nat := let (m, n) := p; m + n

이러한 변형들은 명제를 해체하는 데도 마찬가지로 유용합니다:

variable (p q : Nat Prop) example : ( x, p x) ( y, q y) x y, p x q y | x, px, y, qy => x, y, px, qy example (h₀ : x, p x) (h₁ : y, q y) : x y, p x q y := match h₀, h₁ with | x, px, y, qy => x, y, px, qy example : ( x, p x) ( y, q y) x y, p x q y := fun x, px y, qy => x, y, px, qy example (h₀ : x, p x) (h₁ : y, q y) : x y, p x q y := let x, px := h₀ let y, qy := h₁ x, y, px, qy

8.11. 연습문제🔗

  1. 이름 충돌을 피하기 위해 Hidden 네임스페이스를 열고, 방정식 컴파일러를 사용하여 자연수에 대한 덧셈, 곱셈, 거듭제곱을 정의합니다. 그런 다음 방정식 컴파일러를 사용하여 이들의 기본 성질 몇 가지를 유도합니다.

  2. 마찬가지로, 방정식 컴파일러를 사용하여 리스트에 대한 몇 가지 기본 연산(예를 들어 reverse 함수와 같은)을 정의하고, 귀납법을 사용하여 리스트에 대한 정리를 증명하십시오(임의의 리스트 xs에 대해 reverse (reverse xs) = xs라는 사실과 같은).

  3. 자연수에 대해 값의 경로를 따라가는 재귀(course-of-value recursion)를 수행하는 함수를 직접 정의해 보십시오. 마찬가지로, WellFounded.fix를 스스로 정의하는 방법을 알아낼 수 있는지 시도해 보십시오.

  4. 의존적 패턴 매칭 절의 예제를 따라, 두 벡터를 이어붙이는 함수를 정의하십시오. 이는 까다로운 작업이므로, 보조 함수를 정의해야 합니다.

  5. 다음과 같은 산술 표현식 타입을 생각해 보겠습니다. var n은 변수 vₙ을 나타내고, const n은 값이 n인 상수를 나타낸다는 것이 그 아이디어입니다.

    inductive Expr where | const : Nat Expr | var : Nat Expr | plus : Expr Expr Expr | times : Expr Expr Expr deriving Repr open Expr def sampleExpr : Expr := plus (times (var 0) (const 7)) (times (const 2) (var 1))

    여기서 sampleExpr(v₀ * 7) + (2 * v₁)을 나타냅니다.

    이러한 표현식을 평가하는 함수를 작성하십시오. 이때 각 var nv n으로 평가합니다.

    def declaration uses `sorry`eval (v : Nat Nat) : Expr Nat | const n => sorry | var n => v n | plus e₁ e₂ => sorry | times e₁ e₂ => sorry def sampleVal : Nat Nat | 0 => 5 | 1 => 6 | _ => 0 -- Try it out. You should get 47 here. -- #eval eval sampleVal sampleExpr

    5 + 7과 같은 하위 항을 12로 단순화하는 절차인 “상수 융합(constant fusion)”을 구현하십시오. 보조 함수 simpConst를 사용하여 “fuse”라는 함수를 정의하십시오. 덧셈이나 곱셈을 단순화하려면 먼저 인자들을 재귀적으로 단순화한 다음, 그 결과에 simpConst를 적용하여 단순화를 시도합니다.

    def simpConst : Expr Expr | plus (const n₁) (const n₂) => const (n₁ + n₂) | times (const n₁) (const n₂) => const (n₁ * n₂) | e => e def declaration uses `sorry`fuse : Expr Expr := sorry theorem declaration uses `sorry`simpConst_eq (v : Nat Nat) : e : Expr, eval v (simpConst e) = eval v e := sorry theorem declaration uses `sorry`fuse_eq (v : Nat Nat) : e : Expr, eval v (fuse e) = eval v e := sorry

    마지막 두 정리는 이 정의들이 값을 보존함을 보여줍니다.