Lean 4로 정리 증명하기

7. 귀납적 타입🔗

우리는 Lean의 형식적 기초가 기본 타입인 Prop, Type 0, Type 1, Type 2, ...를 포함하며, 의존 함수 타입인 (x : α) β의 구성을 허용한다는 것을 살펴보았습니다. 예제에서는 Bool, Nat, Int와 같은 추가적인 타입과, List와 같은 타입 생성자, 그리고 곱 ×도 사용했습니다. 실제로 Lean의 라이브러리에서는, 유니버스를 제외한 모든 구체적인 타입과 의존 화살표를 제외한 모든 타입 생성자가 귀납적 타입이라고 알려진 타입 구성의 일반적인 계열의 인스턴스입니다. 타입 유니버스, 의존 화살표 타입, 귀납적 타입만으로도 상당한 규모의 수학 체계를 구축할 수 있다는 것은 주목할 만하며, 그 외의 모든 것은 이들로부터 따라 나옵니다.

직관적으로, 귀납적 타입은 지정된 생성자 목록으로부터 구축됩니다. Lean에서 이러한 타입을 지정하는 구문은 다음과 같습니다:

inductive Foo where
  | constructor₁ : ... → Foo
  | constructor₂ : ... → Foo
  ...
  | constructorₙ : ... → Foo

직관적으로 말해, 각 생성자는 이전에 생성된 값으로부터 새로운 Foo 객체를 만드는 방법을 명시합니다. Foo 타입은 이러한 방식으로 생성된 객체 이외에는 아무것도 포함하지 않습니다.

아래에서 살펴보겠지만, 생성자의 인자는 특정 “양성(positivity)” 제약을 따르는 한 Foo 타입의 객체를 포함할 수 있으며, 이 제약은 Foo의 원소가 아래에서부터 위로 구축됨을 보장합니다. 대략적으로 말하면, 각 ...Foo와 이전에 정의된 타입들로부터 구성된 화살표 타입이라면 무엇이든 될 수 있으며, 여기서 Foo가 등장한다면 그것은 오직 의존 화살표 타입의 “대상(target)”으로서만 등장할 수 있습니다.

귀납적 타입의 예시를 여럿 제시하겠습니다. 또한 위 도식을 상호 정의된 귀납적 타입과 이른바 귀납적 패밀리로 약간 일반화하는 것도 살펴보겠습니다.

논리 연결사와 마찬가지로, 모든 귀납적 타입에는 해당 타입의 원소를 구성하는 방법을 보여 주는 도입 규칙과, 해당 타입의 원소를 다른 구성에서 “사용”하는 방법을 보여 주는 소거 규칙이 함께 옵니다. 논리 연결사와의 유비는 놀라운 일이 아닙니다. 아래에서 보게 되겠지만, 이들 역시 귀납적 타입 구성의 예이기 때문입니다. 귀납적 타입에 대한 도입 규칙은 이미 본 적이 있는데, 이는 바로 해당 타입의 정의에서 명시된 생성자들입니다. 소거 규칙은 해당 타입에 대한 재귀 원리를 제공하며, 이는 특수한 경우로서 귀납법 원리도 포함합니다.

다음 장에서는 Lean의 함수 정의 패키지에 대해 설명하는데, 이는 귀납적 타입에 대한 함수를 정의하고 귀납적 증명을 수행하는 더욱 편리한 방법을 제공합니다. 하지만 귀납적 타입이라는 개념이 매우 근본적이기 때문에, 저수준의 실습적인 이해에서부터 시작하는 것이 중요하다고 생각합니다. 귀납적 타입의 몇 가지 기본적인 예제로 시작하여, 점차 더 정교하고 복잡한 예제로 나아가겠습니다.

7.1. 열거형🔗

가장 간단한 종류의 귀납적 타입은 유한하게 열거된 원소 목록을 가진 타입입니다.

inductive Weekday where | sunday : Weekday | monday : Weekday | tuesday : Weekday | wednesday : Weekday | thursday : Weekday | friday : Weekday | saturday : Weekday

inductive 명령은 새로운 타입 Weekday를 생성합니다. 생성자들은 모두 Weekday 네임스페이스에 속합니다.

Weekday.sunday : Weekday#check Weekday.sunday
Weekday.sunday : Weekday
Weekday.monday : Weekday#check Weekday.monday
Weekday.monday : Weekday
open Weekday Weekday.sunday : Weekday#check sunday
Weekday.sunday : Weekday
Weekday.monday : Weekday#check monday
Weekday.monday : Weekday

Weekday 귀납적 타입을 선언할 때 : Weekday는 생략할 수 있습니다.

inductive Weekday where | sunday | monday | tuesday | wednesday | thursday | friday | saturday

sunday, monday, ... , saturday를 다른 구별되는 특성이 전혀 없는, Weekday의 서로 다른 원소로 생각하십시오. 소거 원리인 Weekday.rec는 타입 Weekday와 그 생성자들과 함께 정의됩니다. 이는 재귀자라고도 알려져 있으며, 타입을 “귀납적”으로 만드는 것이 바로 이것입니다: 각 생성자에 대응하는 값을 지정함으로써 Weekday에 대한 함수를 정의할 수 있게 해줍니다. 직관적으로 귀납적 타입은 생성자들에 의해 남김없이 생성되며, 그것들이 구성하는 것 이외의 원소는 갖지 않습니다.

Weekday.rec.{u} {motive : Weekday Sort u} (sunday : motive Weekday.sunday) (monday : motive Weekday.monday) (tuesday : motive Weekday.tuesday) (wednesday : motive Weekday.wednesday) (thursday : motive Weekday.thursday) (friday : motive Weekday.friday) (saturday : motive Weekday.saturday) (t : Weekday) : motive t

match 표현식을 사용하여 Weekday에서 자연수로 가는 함수를 정의하겠습니다:

open Weekday def numberOfDay (d : Weekday) : Nat := match d with | sunday => 1 | monday => 2 | tuesday => 3 | wednesday => 4 | thursday => 5 | friday => 6 | saturday => 7 1#eval numberOfDay Weekday.sunday
1
2#eval numberOfDay Weekday.monday
2
3#eval numberOfDay Weekday.tuesday
3

Lean의 논리를 사용할 때, match 표현식은 귀납적 타입을 선언할 때 생성되는 재귀자(recursor) Weekday.rec를 사용하여 컴파일됩니다. 이는 결과로 나오는 항이 타입 이론에서 잘 정의되도록 보장합니다. 컴파일된 코드의 경우, match는 다른 함수형 프로그래밍 언어에서와 마찬가지로 컴파일됩니다.

open Weekday def numberOfDay (d : Weekday) : Nat := match d with | sunday => 1 | monday => 2 | tuesday => 3 | wednesday => 4 | thursday => 5 | friday => 6 | saturday => 7 set_option pp.all true def numberOfDay : (d : Weekday) Nat := fun (d : Weekday) => numberOfDay.match_1.{1} (fun (d : Weekday) => Nat) d (fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 1) (instOfNatNat (nat_lit 1))) (fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 2) (instOfNatNat (nat_lit 2))) (fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 3) (instOfNatNat (nat_lit 3))) (fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 4) (instOfNatNat (nat_lit 4))) (fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 5) (instOfNatNat (nat_lit 5))) (fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 6) (instOfNatNat (nat_lit 6))) fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 7) (instOfNatNat (nat_lit 7))#print numberOfDay
def numberOfDay : (d : Weekday)  Nat :=
fun (d : Weekday) =>
  numberOfDay.match_1.{1} (fun (d : Weekday) => Nat) d
    (fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 1) (instOfNatNat (nat_lit 1)))
    (fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 2) (instOfNatNat (nat_lit 2)))
    (fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 3) (instOfNatNat (nat_lit 3)))
    (fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 4) (instOfNatNat (nat_lit 4)))
    (fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 5) (instOfNatNat (nat_lit 5)))
    (fun (_ : Unit) => @OfNat.ofNat.{0} Nat (nat_lit 6) (instOfNatNat (nat_lit 6))) fun (_ : Unit) =>
    @OfNat.ofNat.{0} Nat (nat_lit 7) (instOfNatNat (nat_lit 7))
@[instance_reducible] def numberOfDay.match_1.{u_1} : (motive : Weekday Sort u_1) (d : Weekday) (h_1 : (_ : Unit) motive Weekday.sunday) (h_2 : (_ : Unit) motive Weekday.monday) (h_3 : (_ : Unit) motive Weekday.tuesday) (h_4 : (_ : Unit) motive Weekday.wednesday) (h_5 : (_ : Unit) motive Weekday.thursday) (h_6 : (_ : Unit) motive Weekday.friday) (h_7 : (_ : Unit) motive Weekday.saturday) motive d := fun (motive : Weekday Sort u_1) (d : Weekday) (h_1 : (_ : Unit) motive Weekday.sunday) (h_2 : (_ : Unit) motive Weekday.monday) (h_3 : (_ : Unit) motive Weekday.tuesday) (h_4 : (_ : Unit) motive Weekday.wednesday) (h_5 : (_ : Unit) motive Weekday.thursday) (h_6 : (_ : Unit) motive Weekday.friday) (h_7 : (_ : Unit) motive Weekday.saturday) => @Weekday.casesOn.{u_1} (fun (x : Weekday) => motive x) d (h_1 Unit.unit) (h_2 Unit.unit) (h_3 Unit.unit) (h_4 Unit.unit) (h_5 Unit.unit) (h_6 Unit.unit) (h_7 Unit.unit)#print numberOfDay.match_1
@[instance_reducible] def numberOfDay.match_1.{u_1} : (motive : Weekday  Sort u_1) 
  (d : Weekday) 
    (h_1 : (_ : Unit)  motive Weekday.sunday) 
      (h_2 : (_ : Unit)  motive Weekday.monday) 
        (h_3 : (_ : Unit)  motive Weekday.tuesday) 
          (h_4 : (_ : Unit)  motive Weekday.wednesday) 
            (h_5 : (_ : Unit)  motive Weekday.thursday) 
              (h_6 : (_ : Unit)  motive Weekday.friday)  (h_7 : (_ : Unit)  motive Weekday.saturday)  motive d :=
fun (motive : Weekday  Sort u_1) (d : Weekday) (h_1 : (_ : Unit)  motive Weekday.sunday)
    (h_2 : (_ : Unit)  motive Weekday.monday) (h_3 : (_ : Unit)  motive Weekday.tuesday)
    (h_4 : (_ : Unit)  motive Weekday.wednesday) (h_5 : (_ : Unit)  motive Weekday.thursday)
    (h_6 : (_ : Unit)  motive Weekday.friday) (h_7 : (_ : Unit)  motive Weekday.saturday) =>
  @Weekday.casesOn.{u_1} (fun (x : Weekday) => motive x) d (h_1 Unit.unit) (h_2 Unit.unit) (h_3 Unit.unit)
    (h_4 Unit.unit) (h_5 Unit.unit) (h_6 Unit.unit) (h_7 Unit.unit)
@[reducible] def Weekday.casesOn.{u} : {motive : (t : Weekday) Sort u} (t : Weekday) (sunday : motive Weekday.sunday) (monday : motive Weekday.monday) (tuesday : motive Weekday.tuesday) (wednesday : motive Weekday.wednesday) (thursday : motive Weekday.thursday) (friday : motive Weekday.friday) (saturday : motive Weekday.saturday) motive t := fun {motive : (t : Weekday) Sort u} (t : Weekday) (sunday : motive Weekday.sunday) (monday : motive Weekday.monday) (tuesday : motive Weekday.tuesday) (wednesday : motive Weekday.wednesday) (thursday : motive Weekday.thursday) (friday : motive Weekday.friday) (saturday : motive Weekday.saturday) => @Weekday.rec.{u} motive sunday monday tuesday wednesday thursday friday saturday t#print Weekday.casesOn
@[reducible] def Weekday.casesOn.{u} : {motive : (t : Weekday)  Sort u} 
  (t : Weekday) 
    (sunday : motive Weekday.sunday) 
      (monday : motive Weekday.monday) 
        (tuesday : motive Weekday.tuesday) 
          (wednesday : motive Weekday.wednesday) 
            (thursday : motive Weekday.thursday) 
              (friday : motive Weekday.friday)  (saturday : motive Weekday.saturday)  motive t :=
fun {motive : (t : Weekday)  Sort u} (t : Weekday) (sunday : motive Weekday.sunday) (monday : motive Weekday.monday)
    (tuesday : motive Weekday.tuesday) (wednesday : motive Weekday.wednesday) (thursday : motive Weekday.thursday)
    (friday : motive Weekday.friday) (saturday : motive Weekday.saturday) =>
  @Weekday.rec.{u} motive sunday monday tuesday wednesday thursday friday saturday t
@Weekday.rec.{u_1} : {motive : (t : Weekday) Sort u_1} (sunday : motive Weekday.sunday) (monday : motive Weekday.monday) (tuesday : motive Weekday.tuesday) (wednesday : motive Weekday.wednesday) (thursday : motive Weekday.thursday) (friday : motive Weekday.friday) (saturday : motive Weekday.saturday) (t : Weekday) motive t#check @Weekday.rec
@Weekday.rec.{u_1} : {motive : (t : Weekday)  Sort u_1} 
  (sunday : motive Weekday.sunday) 
    (monday : motive Weekday.monday) 
      (tuesday : motive Weekday.tuesday) 
        (wednesday : motive Weekday.wednesday) 
          (thursday : motive Weekday.thursday) 
            (friday : motive Weekday.friday)  (saturday : motive Weekday.saturday)  (t : Weekday)  motive t

귀납적 데이터 타입을 선언할 때, deriving Repr을 사용하여 Weekday 객체를 텍스트로 변환하는 함수를 생성하도록 Lean에 지시할 수 있습니다. 이 함수는 #eval 명령이 Weekday 객체를 표시하는 데 사용됩니다. Repr이 존재하지 않으면, #eval은 그 자리에서 하나를 유도하려고 시도합니다.

inductive Weekday where | sunday | monday | tuesday | wednesday | thursday | friday | saturday deriving Repr open Weekday Weekday.tuesday#eval tuesday
Weekday.tuesday

어떤 구조와 관련된 정의와 정리를 같은 이름의 네임스페이스로 묶는 것이 유용한 경우가 많습니다. 예를 들어, numberOfDay 함수를 Weekday 네임스페이스에 넣을 수 있습니다. 그러면 해당 네임스페이스를 열었을 때 더 짧은 이름을 사용할 수 있습니다.

Weekday에서 Weekday로의 함수를 정의할 수 있습니다:

namespace Weekday def next (d : Weekday) : Weekday := match d with | sunday => monday | monday => tuesday | tuesday => wednesday | wednesday => thursday | thursday => friday | friday => saturday | saturday => sunday def previous (d : Weekday) : Weekday := match d with | sunday => saturday | monday => sunday | tuesday => monday | wednesday => tuesday | thursday => wednesday | friday => thursday | saturday => friday Weekday.thursday#eval next (next tuesday)
Weekday.thursday
Weekday.tuesday#eval next (previous tuesday)
Weekday.tuesday
example : next (previous tuesday) = tuesday := rfl end Weekday

임의의 Weekday d에 대해 next (previous d) = d라는 일반적인 정리를 어떻게 증명할 수 있을까요? 각 생성자마다 주장에 대한 증명을 제공하기 위해 match를 사용할 수 있습니다:

theorem next_previous (d : Weekday) : next (previous d) = d := match d with | sunday => rfl | monday => rfl | tuesday => rfl | wednesday => rfl | thursday => rfl | friday => rfl | saturday => rfl

택틱 증명을 사용하면 훨씬 더 간결하게 작성할 수 있습니다:

theorem next_previous (d : Weekday) : next (previous d) = d := d:Weekdayd.previous.next = d sunday.previous.next = sundaymonday.previous.next = mondaytuesday.previous.next = tuesdaywednesday.previous.next = wednesdaythursday.previous.next = thursdayfriday.previous.next = fridaysaturday.previous.next = saturday sunday.previous.next = sundaymonday.previous.next = mondaytuesday.previous.next = tuesdaywednesday.previous.next = wednesdaythursday.previous.next = thursdayfriday.previous.next = fridaysaturday.previous.next = saturday All goals completed! 🐙

아래의 귀납적 타입을 위한 택틱에서는 귀납적 타입을 활용하도록 특별히 설계된 추가 택틱들을 소개합니다.

propositions-as-types 대응 관계에서, 함수를 정의할 때뿐만 아니라 정리를 증명할 때도 match를 사용할 수 있다는 점에 주목하십시오. 다시 말해, propositions-as-types 대응 관계에서 경우에 따른 증명은 일종의 경우에 따른 정의이며, 여기서 “정의”되는 것은 데이터 조각이 아니라 증명입니다.

Lean 라이브러리의 Bool 타입은 열거형 타입의 한 예시입니다.

inductive Bool where | false : Bool | true : Bool

(이 예제들을 실행하기 위해, Bool과 같은 이름이 표준 라이브러리의 Bool과 충돌하지 않도록 이들을 Hidden이라는 네임스페이스에 넣었습니다. 이는 이러한 타입들이 시스템이 시작될 때 자동으로 임포트되는 Lean “프렐루드”의 일부이기 때문에 필요합니다.)

연습 문제로서, 이 타입들에 대한 도입 규칙과 소거 규칙이 무엇을 하는지 생각해 보십시오. 추가 연습 문제로, Bool 타입에 불리언 연산 and, or, not을 정의하고 일반적인 항등식을 검증해 볼 것을 제안합니다. and와 같은 이항 연산은 match를 사용해서 정의할 수 있다는 점에 유의하십시오.

def and (a b : Bool) : Bool := match a with | true => b | false => false

마찬가지로, 대부분의 항등식은 적절한 match를 도입한 다음 rfl을 사용하여 증명할 수 있습니다.

7.2. 인자를 갖는 생성자🔗

열거형 타입은 귀납적 타입의 매우 특별한 경우로, 이 경우 생성자가 아무런 인자도 취하지 않습니다. 일반적으로 “구성”은 데이터에 의존할 수 있으며, 그 데이터는 구성된 인자에 표현됩니다. 라이브러리에 있는 곱 타입과 합 타입의 정의를 살펴봅시다.

inductive Prod (α : Type u) (β : Type v) | mk : α β Prod α β inductive Sum (α : Type u) (β : Type v) where | inl : α Sum α β | inr : β Sum α β

이 예제들에서 무슨 일이 일어나고 있는지 살펴봅시다. 곱 타입에는 두 개의 인자를 받는 생성자 Prod.mk 하나가 있습니다. Prod α β에 대한 함수를 정의하려면, 입력이 Prod.mk a b 형태라고 가정할 수 있으며, ab를 이용하여 출력을 명시해야 합니다. 이를 이용하여 Prod에 대한 두 개의 사영을 정의할 수 있습니다. 표준 라이브러리는 Prod α β에 대해 α × β 표기법을, Prod.mk a b에 대해 (a, b) 표기법을 정의한다는 점을 기억하십시오.

def fst {α : Type u} {β : Type v} (p : Prod α β) : α := match p with | Prod.mk a Variable name `b` 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] _b Note: This linter can be disabled with `set_option linter.unusedVariables false`b => a def snd {α : Type u} {β : Type v} (p : Prod α β) : β := match p with | Prod.mk Variable name `a` 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] _a Note: This linter can be disabled with `set_option linter.unusedVariables false`a b => b

함수 fst는 순서쌍 p를 인자로 받습니다. matchp를 순서쌍 Prod.mk a b로 해석합니다. 또한 의존 타입 이론에서 살펴보았듯이, 이 정의들에 가능한 한 최대의 일반성을 부여하기 위해 타입 αβ가 임의의 유니버스에 속하도록 허용한다는 점을 상기하십시오.

다음은 match 대신 재귀자 Prod.casesOn을 사용하는 또 다른 예시입니다.

def prod_example (p : Bool × Nat) : Nat := Prod.casesOn (motive := fun _ => Nat) p (fun b n => cond b (2 * n) (2 * n + 1)) 6#eval prod_example (true, 3)
6
7#eval prod_example (false, 3)
7

인자 motive는 구성하려는 객체의 타입을 지정하는 데 사용되며, 이는 쌍(pair)에 의존할 수 있기 때문에 함수입니다. cond 함수는 불리언 조건문으로, cond b t1 t2b가 참이면 t1을, 그렇지 않으면 t2를 반환합니다. 함수 prod_example은 불리언 b와 숫자 n으로 이루어진 쌍을 받아, b가 참인지 거짓인지에 따라 2 * n 또는 2 * n + 1을 반환합니다.

반면, 합 타입은 inlinr(각각 “왼쪽 삽입”과 “오른쪽 삽입”을 뜻함)이라는 개의 생성자를 가지며, 이들 각각은 하나의 (명시적) 인자를 받습니다. Sum α β에 대한 함수를 정의하려면 두 가지 경우를 처리해야 합니다: 입력이 inl a 형태이면 a를 이용해 출력 값을 지정해야 하고, 입력이 inr b 형태이면 b를 이용해 출력 값을 지정해야 합니다.

def sum_example (s : Sum Nat Nat) : Nat := Sum.casesOn (motive := fun _ => Nat) s (fun n => 2 * n) (fun n => 2 * n + 1) 6#eval sum_example (Sum.inl 3)
6
7#eval sum_example (Sum.inr 3)
7

이 예제는 이전 예제와 유사하지만, 이번에는 sum_example에 대한 입력이 암묵적으로 inl n 또는 inr n 형태 중 하나입니다. 첫 번째 경우 함수는 2 * n을 반환하고, 두 번째 경우에는 2 * n + 1을 반환합니다.

곱 타입이 Prod뿐만 아니라 생성자에도 인자로 쓰이는 매개변수 α β : Type에 의존한다는 점에 주목하십시오. Lean은 이러한 인자가 생성자의 이후 인자나 반환 타입으로부터 추론될 수 있는 경우를 감지하며, 그러한 경우에는 이들을 암묵적 인자로 만듭니다.

자연수 정의하기에서는 귀납적 타입의 생성자가 그 귀납적 타입 자체로부터 인자를 취할 때 어떤 일이 일어나는지 살펴보겠습니다. 이 절에서 다루는 예제들의 특징은 각 생성자가 이전에 지정된 타입에만 의존한다는 점입니다.

생성자가 여러 개인 타입은 선언적임에 유의하십시오. Sum α β의 원소는 inl a 형태이거나 또는 inl b 형태입니다. 인수가 여러 개인 생성자는 결합적 정보를 도입합니다. Prod α β의 원소 Prod.mk a b로부터 a 그리고 b를 추출할 수 있습니다. 임의의 귀납적 타입은 원하는 개수의 생성자를 가지고, 각 생성자가 원하는 개수의 인수를 취함으로써 두 특성을 모두 포함할 수 있습니다.

함수 정의에서와 마찬가지로, Lean의 귀납적 정의 문법에서도 생성자의 이름 있는 인자를 콜론 앞에 둘 수 있습니다.

inductive Prod (α : Type u) (β : Type v) where | mk (fst : α) (snd : β) : Prod α β inductive Sum (α : Type u) (β : Type v) where | inl (a : α) : Sum α β | inr (b : β) : Sum α β

이러한 정의의 결과는 본질적으로 이 절에서 앞서 제시한 것과 동일합니다.

Prod와 같이 생성자가 단 하나뿐인 타입은 순수하게 논리곱적입니다. 즉, 생성자는 단순히 인자 목록을 하나의 데이터로 묶으며, 이는 본질적으로 이후 인자의 타입이 앞선 인자의 타입에 의존할 수 있는 튜플입니다. 이러한 타입은 “레코드” 또는 “구조체”로도 생각할 수 있습니다. Lean에서는 structure 키워드를 사용하여 이러한 귀납적 타입과 그 사영을 동시에 정의할 수 있습니다.

structure Prod (α : Type u) (β : Type v) where mk :: fst : α snd : β

이 예제는 귀납적 타입 Prod와 그 생성자인 mk, 일반적인 소거자(recrecOn), 그리고 위에서 정의된 사영 함수인 fstsnd를 동시에 도입합니다.

생성자의 이름을 지정하지 않으면 Lean은 기본값으로 mk를 사용합니다. 예를 들어, 다음 코드는 색상을 RGB 값의 삼중항으로 저장하는 레코드를 정의합니다.

structure Color where red : Nat green : Nat blue : Nat deriving Repr def yellow := Color.mk 255 255 0 255#eval Color.red yellow
255

yellow의 정의는 제시된 세 값으로 레코드를 구성하며, 프로젝션 Color.red는 빨간색 성분을 반환합니다.

structure 명령은 대수적 구조를 정의하는 데 특히 유용하며, Lean은 이를 다루는 데 필요한 상당한 기반 시설을 제공합니다. 예를 들어, 다음은 반군(semigroup)의 정의입니다:

structure Semigroup where carrier : Type u mul : carrier carrier carrier mul_assoc : a b c, mul (mul a b) c = mul a (mul b c)

더 많은 예제는 구조체와 레코드를 다루는 장에서 살펴보겠습니다.

의존 곱 타입 Sigma에 대해서는 이미 살펴본 바 있습니다:

inductive Sigma {α : Type u} (β : α Type v) where | mk : (a : α) β a Sigma β

라이브러리에 있는 귀납적 타입의 예시를 두 가지 더 들면 다음과 같습니다:

inductive Option (α : Type u) where | none : Option α | some : α Option α inductive Inhabited (α : Type u) where | mk : α Inhabited α

의존 타입 이론의 의미론에는 부분 함수라는 내장된 개념이 없습니다. 함수 타입 α β나 의존 함수 타입 (a : α) β의 모든 원소는 모든 입력에 대해 값을 갖는다고 가정됩니다. Option 타입은 부분 함수를 표현하는 방법을 제공합니다. Option β의 원소는 none이거나, 어떤 값 b : β에 대해 some b 형태입니다. 따라서 타입 α Option β의 원소 fα에서 β로 가는 부분 함수로 생각할 수 있습니다. 모든 a : α에 대해, f af a가 “정의되지 않음”을 나타내는 none을 반환하거나, some b를 반환합니다.

Inhabited α의 원소는 단순히 α의 원소가 존재한다는 사실에 대한 증거입니다. 나중에 우리는 Inhabited가 Lean에서 타입 클래스의 한 예시임을 보게 될 것입니다: Lean에게 적절한 기본 타입들이 거주된다는 것을 알려줄 수 있으며, 이를 바탕으로 다른 구성된 타입들이 거주된다는 것을 자동으로 추론할 수 있습니다.

연습 문제로서, α에서 β로, 그리고 β에서 γ로 가는 부분 함수의 합성 개념을 개발하고, 그것이 예상대로 동작함을 보이시기 바랍니다. 또한 BoolNat이 거주됨을, 두 거주된 타입의 곱이 거주됨을, 그리고 거주된 타입으로 가는 함수의 타입이 거주됨을 보이시기 바랍니다.

7.3. 귀납적으로 정의된 명제🔗

귀납적으로 정의된 타입은 최하위 유니버스인 Prop을 포함하여 어떤 타입 유니버스에도 존재할 수 있습니다. 실제로 논리 연결사가 정의되는 방식이 바로 이것입니다.

inductive False : Prop inductive True : Prop where | intro : True inductive And (a b : Prop) : Prop where | intro : a b And a b inductive Or (a b : Prop) : Prop where | inl : a Or a b | inr : b Or a b

이러한 것들이 여러분이 이미 살펴본 도입 규칙과 소거 규칙을 어떻게 발생시키는지 생각해 보아야 합니다. 귀납적 타입의 소거자가 어디로 소거할 수 있는지, 즉 어떤 종류의 타입이 재귀자의 대상이 될 수 있는지를 지배하는 규칙들이 있습니다. 대략적으로 말하면, Prop에 속한 귀납적 타입을 특징짓는 것은 오직 Prop에 속한 다른 타입으로만 소거할 수 있다는 점입니다. 이는 p : Prop일 때 원소 hp : p가 어떤 데이터도 담지 않는다는 이해와 일치합니다. 하지만 이 규칙에는 작은 예외가 하나 있는데, 이는 아래 귀납적 패밀리에서 다루겠습니다.

존재 한정사조차도 귀납적으로 정의됩니다:

inductive Exists {α : Sort u} (p : α Prop) : Prop where | intro (w : α) (h : p w) : Exists p

표기법 x : α, pExists (fun x : α => p)의 문법적 설탕(syntactic sugar)임을 유념하십시오.

False, True, And, Or의 정의는 Empty, Unit, Prod, Sum의 정의와 완벽하게 유사합니다. 차이점은 첫 번째 그룹은 Prop의 원소를 산출하고, 두 번째 그룹은 어떤 u에 대해 Type u의 원소를 산출한다는 것입니다. 이와 유사한 방식으로, x : α, pΣ x : α, βProp 값 변형입니다.

여기서 {x : α // p}로 표시되는 또 다른 귀납적 타입을 언급하는 것이 좋겠습니다. 이는 x : α, pΣ x : α, β의 일종의 혼합체입니다.

inductive Subtype {α : Type u} (p : α Prop) where | mk : (x : α) p x Subtype p

실제로 Lean에서 Subtype은 structure 명령을 사용하여 정의됩니다:

structure Subtype {α : Sort u} (p : α Prop) where val : α property : p val

{x : α // p x} 표기법은 Subtype (fun x : α => p x)의 문법적 설탕(syntactic sugar)입니다. 이는 집합론에서의 부분집합 표기법을 본떠 만들어졌습니다. 즉, {x : α // p x}α의 원소 중 속성 p를 만족하는 것들의 모임을 나타낸다는 발상입니다.

7.4. 자연수 정의하기🔗

지금까지 살펴본 귀납적으로 정의된 타입들은 “평평”합니다: 생성자는 데이터를 감싸서 타입에 삽입하며, 그에 대응하는 재귀자는 데이터를 풀어서 그것에 대해 작동합니다. 생성자가 정의되고 있는 바로 그 타입의 원소들에 대해 작동할 때 상황은 훨씬 더 흥미로워집니다. 자연수 타입 Nat이 대표적인 예입니다:

inductive Nat where | zero : Nat | succ : Nat Nat

생성자는 두 개가 있습니다. 먼저 zero : Nat부터 시작하는데, 이는 인자를 받지 않으므로 처음부터 가지고 있는 셈입니다. 이와 대조적으로, 생성자 succ은 이전에 구성된 Nat에만 적용할 수 있습니다. 이를 zero에 적용하면 succ zero : Nat이 만들어집니다. 이를 다시 적용하면 succ (succ zero) : Nat이 만들어지며, 이런 식으로 계속됩니다. 직관적으로 Nat은 이러한 생성자들을 갖는 “가장 작은” 타입이며, 이는 zero에서 시작하여 succ을 반복적으로 적용함으로써 남김없이 (그리고 자유롭게) 생성됨을 의미합니다.

이전과 마찬가지로, Nat에 대한 재귀자는 Nat에서 임의의 정의역으로 향하는 의존 함수 f, 즉 어떤 motive : Nat Sort u에 대해 (n : Nat) motive n의 원소인 f를 정의하도록 설계되어 있습니다. 이 재귀자는 두 가지 경우를 처리해야 하는데, 입력이 zero인 경우와 입력이 어떤 n : Nat에 대해 succ n 형태인 경우입니다. 첫 번째 경우에는, 이전과 마찬가지로 알맞은 타입을 갖는 목표 값을 그냥 지정합니다. 그러나 두 번째 경우에는, 재귀자가 n에서의 f의 값이 이미 계산되어 있다고 가정할 수 있습니다. 그 결과, 재귀자의 다음 인자는 nf n을 이용하여 f (succ n)의 값을 지정합니다. 재귀자의 타입을 확인해 보면 다음과 같습니다.

Nat.rec.{u} : {motive : Nat Sort u} (zero : motive Nat.zero) (succ : (n : Nat) motive n motive (Nat.succ n)) (t : Nat) motive t

암시적 인자인 motive는 정의되는 함수의 공역입니다. 타입 이론에서는 motive가 소거/재귀의 동기라고 흔히 말하는데, 이는 우리가 구성하고자 하는 대상의 종류를 나타내기 때문입니다. 다음 두 인자는 위에서 설명한 것처럼 0의 경우와 후행자의 경우를 계산하는 방법을 명시합니다. 이들은 부전제라고도 알려져 있습니다. 마지막으로, t : Nat은 함수의 입력입니다. 이는 주전제라고도 알려져 있습니다.

Nat.recOnNat.rec과 유사하지만, 주요 전제가 부차 전제들보다 앞에 옵니다.

Nat.recOn.{u} : {motive : Nat Sort u} (t : Nat) (zero : motive Nat.zero) (succ : ((n : Nat) motive n motive (Nat.succ n))) motive t

예를 들어, 자연수에 대한 덧셈 함수 add m n을 생각해 봅시다. m을 고정하면, n에 대한 재귀법으로 덧셈을 정의할 수 있습니다. 기본 단계에서는 add m zerom으로 둡니다. 후행자 단계에서는, add m n의 값이 이미 결정되어 있다고 가정하고, add m (succ n)succ (add m n)으로 정의합니다.

inductive Nat where | zero : Nat | succ : Nat Nat deriving Repr def add (m n : Nat) : Nat := match n with | Nat.zero => m | Nat.succ n => Nat.succ (add m n) open Ambiguous namespace `Nat`: it is interpreted as `_root_.Hidden.Nat` because this `open` occurs inside `namespace Hidden`, while `_root_.Nat` is silently not opened. Specify the namespace unambiguously, e.g. `_root_.Hidden.Nat`. The warning can sometimes also be addressed by moving the `open` outside of the surrounding `namespace`. Note: This linter can be disabled with `set_option linter.ambiguousOpen false`Nat Hidden.Nat.succ (Hidden.Nat.succ (Hidden.Nat.succ (Hidden.Nat.zero)))#eval add (succ (succ zero)) (succ zero)
Hidden.Nat.succ (Hidden.Nat.succ (Hidden.Nat.succ (Hidden.Nat.zero)))

이러한 정의는 Nat 네임스페이스에 넣어두는 것이 유용합니다. 그런 다음 이 네임스페이스에서 익숙한 표기법을 정의할 수 있습니다. 덧셈에 대한 두 정의 방정식은 이제 정의적으로 성립합니다:

namespace Nat def add (m n : Nat) : Nat := match n with | Nat.zero => m | Nat.succ n => Nat.succ (add m n) instance : Add Nat where add := add theorem add_zero (m : Nat) : m + zero = m := rfl theorem add_succ (m n : Nat) : m + succ n = succ (m + n) := rfl end Nat

instance 명령이 어떻게 작동하는지는 타입 클래스 장에서 설명하겠습니다. 아래 예제에서는 Lean 버전의 자연수를 사용하겠습니다.

하지만 0 + n = n과 같은 사실을 증명하려면 귀납법에 의한 증명이 필요합니다. 앞서 살펴본 바와 같이, 공역 motive nProp의 원소인 경우 귀납법 원리는 재귀 원리의 특수한 경우일 뿐입니다. 이는 귀납적 증명의 익숙한 패턴을 나타냅니다: n, motive n을 증명하려면 먼저 motive 0을 증명하고, 그다음 임의의 n에 대해 ih : motive n을 가정하고 motive (n + 1)을 증명합니다.

open Nat theorem zero_add (n : Nat) : 0 + n = n := Nat.recOn (motive := fun x => 0 + x = x) n (show 0 + 0 = 0 from rfl) (fun (n : Nat) (ih : 0 + n = n) => show 0 + (n + 1) = n + 1 from calc 0 + (n + 1) _ = (0 + n) + 1 := rfl _ = n + 1 := n✝:Natn:Natih:0 + n = n0 + n + 1 = n + 1 All goals completed! 🐙)

다시 한번 주목할 점은, 증명의 맥락에서 Nat.recOn이 사용될 때 이는 실제로 귀납법 원리를 변장한 것에 지나지 않는다는 것입니다. rwsimp 택틱은 이러한 증명에서 매우 효과적인 경향이 있습니다. 이 경우 각 택틱을 사용하여 증명을 다음과 같이 축소할 수 있습니다:

open Nat theorem zero_add (n : Nat) : 0 + n = n := Nat.recOn (motive := fun x => 0 + x = x) n rfl (fun n ih => n✝:Natn:Natih:0 + n = n0 + n.succ = n.succ All goals completed! 🐙)

다른 예로, 덧셈의 결합법칙 m n k, m + n + k = m + (n + k)을 증명해 봅시다. (우리가 정의한 대로 표기법 +는 왼쪽으로 결합하므로, m + n + k는 실제로는 (m + n) + k입니다.) 가장 어려운 부분은 어떤 변수에 대해 귀납법을 적용할지 알아내는 것입니다. 덧셈은 두 번째 인자에 대한 재귀로 정의되므로 k가 좋은 추측이며, 일단 이 선택을 하고 나면 증명은 거의 저절로 작성됩니다.

open Nat theorem add_assoc (m n k : Nat) : m + n + k = m + (n + k) := Nat.recOn (motive := fun k => m + n + k = m + (n + k)) k (show m + n + 0 = m + (n + 0) from rfl) (fun k (ih : m + n + k = m + (n + k)) => show m + n + (k + 1) = m + (n + (k + 1)) from calc m + n + (k + 1) _ = (m + n + k) + 1 := rfl _ = (m + (n + k)) + 1 := m:Natn:Natk✝:Natk:Natih:m + n + k = m + (n + k)m + n + k + 1 = m + (n + k) + 1 All goals completed! 🐙 _ = m + ((n + k) + 1) := rfl _ = m + (n + (k + 1)) := rfl)

이번에도 증명을 다음과 같이 줄일 수 있습니다:

open Nat theorem add_assoc (m n k : Nat) : m + n + k = m + (n + k) := Nat.recOn (motive := fun k => m + n + k = m + (n + k)) k rfl (fun k ih => m:Natn:Natk✝:Natk:Natih:m + n + k = m + (n + k)m + n + k.succ = m + (n + k.succ) m:Natn:Natk✝:Natk:Natih:m + n + k = m + (n + k)m + (n + k) + 1 = m + (n + (k + 1)); All goals completed! 🐙)

덧셈의 교환법칙을 증명하려 한다고 가정해 봅시다. 두 번째 인자에 대한 귀납법을 선택하면, 다음과 같이 시작할 수 있습니다.

open Nat theorem declaration uses `sorry`add_comm (m n : Nat) : m + n = n + m := Nat.recOn (motive := fun x => m + x = x + m) n (show m + 0 = 0 + m All goals completed! 🐙 All goals completed! 🐙) (fun (n : Nat) (ih : m + n = n + m) => show m + succ n = succ n + m from calc m + succ n _ = succ (m + n) := rfl _ = succ (n + m) := m:Natn✝:Natn:Natih:m + n = n + m(m + n).succ = (n + m).succ All goals completed! 🐙 _ = succ n + m := sorry)

이 시점에서 우리는 또 다른 보조 사실, 즉 succ (n + m) = succ n + m이 필요함을 알 수 있습니다. m에 대한 귀납법으로 이를 증명할 수 있습니다:

open Nat theorem succ_add (n m : Nat) : succ n + m = succ (n + m) := Nat.recOn (motive := fun x => succ n + x = succ (n + x)) m (show succ n + 0 = succ (n + 0) from rfl) (fun (m : Nat) (ih : succ n + m = succ (n + m)) => show succ n + succ m = succ (n + succ m) from calc succ n + succ m _ = succ (succ n + m) := rfl _ = succ (succ (n + m)) := n:Natm✝:Natm:Natih:n.succ + m = (n + m).succ(n.succ + m).succ = (n + m).succ.succ All goals completed! 🐙 _ = succ (n + succ m) := rfl)

그러면 이전 증명의 sorrysucc_add로 대체할 수 있습니다. 다시 한번, 증명을 압축할 수 있습니다:

open Ambiguous namespace `Nat`: it is interpreted as `_root_.Hidden.Nat` because this `open` occurs inside `namespace Hidden`, while `_root_.Nat` is silently not opened. Specify the namespace unambiguously, e.g. `_root_.Hidden.Nat`. The warning can sometimes also be addressed by moving the `open` outside of the surrounding `namespace`. Note: This linter can be disabled with `set_option linter.ambiguousOpen false`Nat theorem succ_add (n m : Nat) : succ n + m = succ (n + m) := Nat.recOn (motive := fun x => succ n + x = succ (n + x)) m rfl (fun m ih => n:Natm✝:Natm:Natih:n.succ + m = (n + m).succn.succ + m.succ = (n + m.succ).succ All goals completed! 🐙) theorem add_comm (m n : Nat) : m + n = n + m := Nat.recOn (motive := fun x => m + x = x + m) n (m:Natn:Natm + zero = zero + m All goals completed! 🐙) (fun m ih => m✝:Natn:Natm:Natih:m✝ + m = m + m✝m✝ + m.succ = m.succ + m✝ All goals completed! 🐙)

7.5. 그 밖의 재귀적 데이터 타입🔗

귀납적으로 정의된 타입의 예시를 몇 가지 더 살펴봅시다. 임의의 타입 α에 대해, α의 원소들로 이루어진 리스트의 타입 List α가 라이브러리에 정의되어 있습니다.

inductive List (α : Type u) where | nil : List α | cons (h : α) (t : List α) : List α namespace List def append (as bs : List α) : List α := match as with | nil => bs | cons a as => cons a (append as bs) theorem nil_append (as : List α) : append nil as = as := rfl theorem cons_append (a : α) (as bs : List α) : append (cons a as) bs = cons a (append as bs) := rfl end List

α 타입 원소들의 리스트는 빈 리스트 nil이거나, 원소 h : α와 그 뒤에 이어지는 리스트 t : List α로 이루어진 것입니다. 첫 번째 원소인 h는 흔히 리스트의 “머리(head)”라 불리며, 나머지인 t는 “꼬리(tail)”라 불립니다.

연습 문제로, 다음을 증명하십시오:

theorem declaration uses `sorry`append_nil (as : List α) : append as nil = as := sorry theorem declaration uses `sorry`append_assoc (as bs cs : List α) : append (append as bs) cs = append as (append bs cs) := sorry

리스트의 길이를 반환하는 함수 length : {α : Type u} List α Nat도 정의해 보고, 이 함수가 예상대로 동작함을 증명해 보십시오 (예를 들어, length (append as bs) = length as + length bs).

또 다른 예로, 이진 트리 타입을 다음과 같이 정의할 수 있습니다:

inductive BinaryTree where | leaf : BinaryTree | node : BinaryTree BinaryTree BinaryTree

실제로 가산 개의 가지를 갖는 트리의 타입까지도 정의할 수 있습니다:

inductive CBTree where | leaf : CBTree | sup : (Nat CBTree) CBTree namespace CBTree def succ (t : CBTree) : CBTree := sup (fun _ => t) def toCBTree : Nat CBTree | 0 => leaf | n+1 => succ (toCBTree n) def omega : CBTree := sup toCBTree end CBTree

7.6. 귀납적 타입을 위한 택틱🔗

Lean에서 귀납적 타입이 지니는 근본적인 중요성을 고려할 때, 이를 효과적으로 다루도록 설계된 다양한 택틱이 존재한다는 사실은 그리 놀라운 일이 아닙니다. 여기서는 그중 일부를 설명합니다.

cases 택틱은 귀납적으로 정의된 타입의 원소에 대해 작동하며, 그 이름이 시사하는 대로 동작합니다. 즉, 가능한 각 생성자에 따라 해당 원소를 분해합니다. 가장 기본적인 형태로는 로컬 컨텍스트에 있는 원소 x에 적용됩니다. 그러면 목표는 x가 각 구성 요소로 대체된 경우들로 축소됩니다.

example (p : Nat Prop) (hz : p 0) (hs : n, p (Nat.succ n)) : n, p n := p:Nat Prophz:p 0hs: (n : Nat), p n.succ (n : Nat), p n p:Nat Prophz:p 0hs: (n : Nat), p n.succn:Natp n p:Nat Prophz:p 0hs: (n : Nat), p n.succp 0p:Nat Prophz:p 0hs: (n : Nat), p n.succn✝:Natp (n✝ + 1) p:Nat Prophz:p 0hs: (n : Nat), p n.succp 0 All goals completed! 🐙 p:Nat Prophz:p 0hs: (n : Nat), p n.succn✝:Natp (n✝ + 1) All goals completed! 🐙

첫 번째 분기에서 증명 상태는 다음과 같습니다:

p:Nat Prophz:p 0hs: (n : Nat), p n.succp 0

두 번째 분기에서는 다음과 같습니다:

p:Nat Prophz:p 0hs: (n : Nat), p n.succn✝:Natp (n✝ + 1)

추가적인 부가 기능들도 있습니다. 한 가지 예로, caseswith 절을 사용하여 각 경우의 이름을 선택할 수 있게 해줍니다. 예를 들어 다음 예제에서는 succ의 인자에 m이라는 이름을 선택하여, 두 번째 경우가 succ m을 참조하도록 합니다. 더 중요한 점은, cases 택틱이 지역 문맥에서 대상 변수에 의존하는 항목들을 감지한다는 것입니다. 이 택틱은 이러한 요소들을 되돌린 뒤, 분할을 수행하고, 다시 도입합니다. 아래 예제에서, 가설 h : n 0이 첫 번째 분기에서는 h : 0 0이 되고, 두 번째 분기에서는 h : m + 1 0이 되는 것에 주목하십시오.

open Nat example (n : Nat) (h : n 0) : succ (pred n) = n := n:Nath:n 0n.pred.succ = n cases n with h:0 0(pred 0).succ = 0 All goals completed! 🐙 m:Nath:m + 1 0(m + 1).pred.succ = m + 1 All goals completed! 🐙

cases는 명제를 증명하는 데뿐만 아니라 데이터를 생성하는 데에도 사용할 수 있다는 점에 주목하십시오.

def f (n : Nat) : Nat := n:NatNat Natn✝:NatNat; n✝:NatNat; All goals completed! 🐙 example : f 0 = 3 := rfl example : f 5 = 7 := rfl

이번에도 케이스들은 컨텍스트에서 의존성을 되돌리고, 분할한 다음, 다시 도입할 것입니다.

def Tuple (α : Type) (n : Nat) := { as : List α // as.length = n } def f {n : Nat} (t : Tuple α n) : Nat := α:Typen:Natt:Tuple α nNat α:Typet:Tuple α 0Natα:Typen✝:Natt:Tuple α (n✝ + 1)Nat; α:Typen✝:Natt:Tuple α (n✝ + 1)Nat; All goals completed! 🐙 def myTuple : Tuple Nat 3 := [0, 1, 2], rfl example : f myTuple = 7 := rfl

다음은 인자가 있는 여러 생성자의 예시입니다.

inductive Foo where | bar1 : Nat Nat Foo | bar2 : Nat Nat Nat Foo def silly (x : Foo) : Nat := x:FooNat cases x with a:Natb:NatNat All goals completed! 🐙 c:Natd:Nate:NatNat All goals completed! 🐙

각 생성자에 대한 대안이 반드시 생성자가 선언된 순서대로 해결될 필요는 없습니다.

def silly (x : Foo) : Nat := x:FooNat cases x with c:Natd:Nate:NatNat All goals completed! 🐙 a:Natb:NatNat All goals completed! 🐙

with의 문법은 구조화된 증명을 작성하는 데 편리합니다. Lean은 이를 보완하는 case 택틱도 제공하는데, 이를 통해 목표에 집중하며 변수 이름을 지정할 수 있습니다.

def silly (x : Foo) : Nat := x:FooNat a✝¹:Nata✝:NatNata✝²:Nata✝¹:Nata✝:NatNat case bar1 a b a:Natb:NatNat All goals completed! 🐙 case bar2 c d e c:Natd:Nate:NatNat All goals completed! 🐙

case 택틱은 영리해서, 생성자를 적절한 목표에 맞춰 매칭해 줍니다. 예를 들어, 위의 목표들을 반대 순서로 채울 수 있습니다.

def silly (x : Foo) : Nat := x:FooNat a✝¹:Nata✝:NatNata✝²:Nata✝¹:Nata✝:NatNat case bar2 c d e c:Natd:Nate:NatNat All goals completed! 🐙 case bar1 a b a:Natb:NatNat All goals completed! 🐙

cases를 임의의 표현식과 함께 사용할 수도 있습니다. 해당 표현식이 목표에 나타난다고 가정하면, cases 택틱은 그 표현식에 대해 일반화한 뒤, 그 결과로 생기는 전칭 양화된 변수를 도입하고, 그 변수에 대해 case 분석을 수행합니다.

open Nat example (p : Nat Prop) (hz : p 0) (hs : n, p (succ n)) (m k : Nat) : p (m + 3 * k) := p:Nat Prophz:p 0hs: (n : Nat), p n.succm:Natk:Natp (m + 3 * k) p:Nat Prophz:p 0hs: (n : Nat), p n.succm:Natk:Natp 0p:Nat Prophz:p 0hs: (n : Nat), p n.succm:Natk:Natn✝:Natp (n✝ + 1) p:Nat Prophz:p 0hs: (n : Nat), p n.succm:Natk:Natn✝:Natp (n✝ + 1) -- goal is p 0 All goals completed! 🐙 -- goal is a : Nat ⊢ p (succ a)

이를 “m + 3 * k가 0인지 아니면 어떤 수의 successor인지에 대해 경우를 나눈다”고 말하는 것으로 생각하십시오. 그 결과는 기능적으로 다음과 동등합니다:

open Nat example (p : Nat Prop) (hz : p 0) (hs : n, p (succ n)) (m k : Nat) : p (m + 3 * k) := p:Nat Prophz:p 0hs: (n : Nat), p n.succm:Natk:Natp (m + 3 * k) p:Nat Prophz:p 0hs: (n : Nat), p n.succm:Natk:Natn:Natp n p:Nat Prophz:p 0hs: (n : Nat), p n.succm:Natk:Natp 0p:Nat Prophz:p 0hs: (n : Nat), p n.succm:Natk:Natn✝:Natp (n✝ + 1) p:Nat Prophz:p 0hs: (n : Nat), p n.succm:Natk:Natn✝:Natp (n✝ + 1) All goals completed! 🐙

m + 3 * kgeneralize에 의해 지워진다는 점에 유의하십시오. 중요한 것은 오직 그것이 0 형태인지 아니면 n✝ + 1 형태인지뿐입니다. 이 형태의 cases는 방정식에서 해당 식(이 경우 m + 3 * k)을 함께 언급하는 어떤 가설도 되돌리지 않습니다. 만약 그러한 항이 가설에 나타나고 그것도 함께 일반화하고자 한다면, 명시적으로 revert해야 합니다.

케이스 분석하는 표현식이 목표에 나타나지 않으면, cases 택틱은 have를 사용하여 해당 표현식의 타입을 맥락에 넣습니다. 다음은 예시입니다:

example (p : Prop) (m n : Nat) (h₁ : m < n p) (h₂ : m n p) : p := p:Propm:Natn:Nath₁:m < n ph₂:m n pp p:Propm:Natn:Nath₁:m < n ph₂:m n ph✝:m < npp:Propm:Natn:Nath₁:m < n ph₂:m n ph✝:m np case inl hlt p:Propm:Natn:Nath₁:m < n ph₂:m n phlt:m < np All goals completed! 🐙 case inr hge p:Propm:Natn:Nath₁:m < n ph₂:m n phge:m np All goals completed! 🐙

정리 Nat.lt_or_ge m nm < nm n을 말하며, 위 증명을 이 두 경우로 나누는 것으로 생각하는 것이 자연스럽습니다. 첫 번째 분기에서는 가설 hlt : m < n이 있고, 두 번째 분기에서는 가설 hge : m n이 있습니다. 위 증명은 다음과 기능적으로 동등합니다.

example (p : Prop) (m n : Nat) (h₁ : m < n p) (h₂ : m n p) : p := p:Propm:Natn:Nath₁:m < n ph₂:m n pp p:Propm:Natn:Nath₁:m < n ph₂:m n ph:m < n m np p:Propm:Natn:Nath₁:m < n ph₂:m n ph✝:m < npp:Propm:Natn:Nath₁:m < n ph₂:m n ph✝:m np case inl hlt p:Propm:Natn:Nath₁:m < n ph₂:m n phlt:m < np All goals completed! 🐙 case inr hge p:Propm:Natn:Nath₁:m < n ph₂:m n phge:m np All goals completed! 🐙

첫 두 줄 다음에는 가설로 h : m < n m n이 있으며, 우리는 단순히 이에 대해 경우 나누기를 합니다.

다음은 자연수에 대한 동치의 결정 가능성을 이용해 m = nm n인 경우로 나누는 또 다른 예시입니다.

Nat.sub_self (n : Nat) : n - n = 0#check Nat.sub_self
Nat.sub_self (n : Nat) : n - n = 0
example (m n : Nat) : m - n = 0 m n := m:Natn:Natm - n = 0 m n cases Decidable.em (m = n) with m:Natn:Natheq:m = nm - n = 0 m n m:Natn:Natheq:m = nn - n = 0 n n; m:Natn:Natheq:m = nn - n = 0; All goals completed! 🐙 m:Natn:Nathne:¬m = nm - n = 0 m n m:Natn:Nathne:¬m = nm n; All goals completed! 🐙

open Classical을 하면 어떤 명제에 대해서든 배중률을 사용할 수 있다는 점을 기억하십시오. 하지만 타입 클래스 추론(참고: 타입 클래스)을 이용하면 Lean이 실제로 관련 결정 절차를 찾아낼 수 있으며, 이는 계산 가능한 함수에서도 그 경우 분기를 사용할 수 있다는 의미입니다.

cases 택틱을 사용하여 경우 나누기 증명을 수행할 수 있는 것처럼, induction 택틱을 사용하여 귀납법에 의한 증명을 수행할 수 있습니다. 구문은 cases의 구문과 유사하지만, 인자가 지역 문맥의 항일 수만 있다는 점이 다릅니다. 다음은 예시입니다:

theorem zero_add (n : Nat) : 0 + n = n := n:Nat0 + n = n induction n with 0 + 0 = 0 All goals completed! 🐙 n:Natih:0 + n = n0 + (n + 1) = n + 1 All goals completed! 🐙

cases와 마찬가지로, with 대신 case 택틱을 사용할 수 있습니다.

theorem zero_add (n : Nat) : 0 + n = n := n:Nat0 + n = n 0 + 0 = 0n✝:Nata✝:0 + n✝ = n✝0 + (n✝ + 1) = n✝ + 1 case zero 0 + 0 = 0 All goals completed! 🐙 case succ n ih n:Natih:0 + n✝ = n✝0 + (n✝ + 1) = n✝ + 1 All goals completed! 🐙

다음은 몇 가지 추가 예시입니다.

open Ambiguous namespace `Nat`: it is interpreted as `_root_.Hidden.Nat` because this `open` occurs inside `namespace Hidden`, while `_root_.Nat` is silently not opened. Specify the namespace unambiguously, e.g. `_root_.Hidden.Nat`. The warning can sometimes also be addressed by moving the `open` outside of the surrounding `namespace`. Note: This linter can be disabled with `set_option linter.ambiguousOpen false`Nat theorem zero_add (n : Nat) : 0 + n = n := n:Nat0 + n = n 0 + zero = zeroa✝:Nata_ih✝:0 + a✝ = a✝0 + a✝.succ = a✝.succ 0 + zero = zeroa✝:Nata_ih✝:0 + a✝ = a✝0 + a✝.succ = a✝.succ All goals completed! 🐙 theorem succ_add (m n : Nat) : succ m + n = succ (m + n) := m:Natn:Natm.succ + n = (m + n).succ m:Natm.succ + zero = (m + zero).succm:Nata✝:Nata_ih✝:m.succ + a✝ = (m + a✝).succm.succ + a✝.succ = (m + a✝.succ).succ m:Natm.succ + zero = (m + zero).succm:Nata✝:Nata_ih✝:m.succ + a✝ = (m + a✝).succm.succ + a✝.succ = (m + a✝.succ).succ All goals completed! 🐙 theorem add_comm (m n : Nat) : m + n = n + m := m:Natn:Natm + n = n + m m:Natm + zero = zero + mm:Nata✝:Nata_ih✝:m + a✝ = a✝ + mm + a✝.succ = a✝.succ + m m:Natm + zero = zero + mm:Nata✝:Nata_ih✝:m + a✝ = a✝ + mm + a✝.succ = a✝.succ + m All goals completed! 🐙 theorem add_assoc (m n k : Nat) : m + n + k = m + (n + k) := m:Natn:Natk:Natm + n + k = m + (n + k) m:Natn:Natm + n + zero = m + (n + zero)m:Natn:Nata✝:Nata_ih✝:m + n + a✝ = m + (n + a✝)m + n + a✝.succ = m + (n + a✝.succ) m:Natn:Natm + n + zero = m + (n + zero)m:Natn:Nata✝:Nata_ih✝:m + n + a✝ = m + (n + a✝)m + n + a✝.succ = m + (n + a✝.succ) All goals completed! 🐙

induction 택틱은 여러 대상(일명 주요 전제)을 가지는 사용자 정의 귀납법 원리도 지원합니다. 이 예제는 Nat.mod.inductionOn을 사용하며, 이는 다음과 같은 시그니처를 가지고 있습니다:

Nat.mod.inductionOn {motive : Nat Nat Sort u} (x y : Nat) (ind : x y, 0 < y y x motive (x - y) y motive x y) (base : x y, ¬(0 < y y x) motive x y) : motive x y
example (x : Nat) {y : Nat} (h : y > 0) : x % y < y := x:Naty:Nath:y > 0x % y < y induction x, y using Nat.mod.inductionOn with x:Naty:Nath₁:0 < y y xih:y > 0 (x - y) % y < yh:y > 0x % y < y x:Naty:Nath₁:0 < y y xih:y > 0 (x - y) % y < yh:y > 0(x - y) % y < y All goals completed! 🐙 x:Naty:Nath₁:¬(0 < y y x)h:y > 0x % y < y x:Naty:Nath₁:¬(0 < y y x)h:y > 0this:¬0 < y ¬y xx % y < y match this with x:Naty:Nath₁✝:¬(0 < y y x)h:y > 0this:¬0 < y ¬y xh₁:¬0 < yx % y < y All goals completed! 🐙 x:Naty:Nath₁✝:¬(0 < y y x)h:y > 0this:¬0 < y ¬y xh₁:¬y xx % y < y x:Naty:Nath₁✝:¬(0 < y y x)h:y > 0this:¬0 < y ¬y xh₁:¬y xhgt:y > xx % y < y x:Naty:Nath₁✝:¬(0 < y y x)h:y > 0this:¬0 < y ¬y xh₁:¬y xhgt:y > x % yx % y < y All goals completed! 🐙

택틱에서도 match 표기법을 사용할 수 있습니다:

example : p q q p := p:Propq:Propp q q p p:Propq:Proph:p qq p match h with p:Propq:Proph:p qh✝:pq p p:Propq:Proph:p qh✝:pp; All goals completed! 🐙 p:Propq:Proph:p qh2:qq p p:Propq:Proph:p qh2:qq; All goals completed! 🐙

편의를 위해 패턴 매칭은 introfunext와 같은 택틱에 통합되어 있습니다.

example : s q r p r q p := s:Propq:Propr:Propp:Props q r p r q p s:Propq:Propr:Propp:Propleft✝:shq:qright✝¹:rhp:pright✝:rq p All goals completed! 🐙 example : (fun (x : Nat × Nat) (y : Nat × Nat) => x.1 + y.2) = (fun (x : Nat × Nat) (z : Nat × Nat) => z.2 + x.1) := (fun x y => x.fst + y.snd) = fun x z => z.snd + x.fst a:Natb:Natc:Natd:Nat(a, b).fst + (c, d).snd = (c, d).snd + (a, b).fst a:Natb:Natc:Natd:Nata + d = d + a All goals completed! 🐙

귀납적 타입을 다루는 작업을 용이하게 하기 위해 설계된 마지막 택틱인 injection 택틱으로 이 절을 마무리합니다. 설계상 귀납적 타입의 원소들은 자유롭게 생성되는데, 이는 곧 생성자가 단사적이며 서로소인 치역을 가진다는 것을 의미합니다. injection 택틱은 이 사실을 활용하도록 설계되어 있습니다:

open Nat example (m n k : Nat) (h : succ (succ m) = succ (succ n)) : n + k = m + k := m:Natn:Natk:Nath:m.succ.succ = n.succ.succn + k = m + k m:Natn:Natk:Nath':m + 1 = n + 1n + k = m + k m:Natn:Natk:Nath'':m = nn + k = m + k All goals completed! 🐙

택틱의 첫 번째 인스턴스는 문맥에 h' : m + 1 = n + 1을 추가하고, 두 번째 인스턴스는 h'' : m = n을 추가합니다.

injection 택틱은 서로 다른 생성자가 같다고 놓였을 때 발생하는 모순도 감지하며, 이를 이용해 목표를 닫습니다.

open Nat example (m n : Nat) (h : succ m = 0) : n = n + 7 := m:Natn:Nath:m.succ = 0n = n + 7 All goals completed! 🐙 example (m n : Nat) (h : succ m = 0) : n = n + 7 := m:Natn:Nath:m.succ = 0n = n + 7 All goals completed! 🐙 example (h : 7 = 4) : False := h:7 = 4False All goals completed! 🐙

두 번째 예제에서 볼 수 있듯이, contradiction 택틱 또한 이러한 형태의 모순을 탐지합니다.

7.7. 귀납적 패밀리🔗

Lean이 허용하는 귀납적 정의의 전체 범위를 설명하는 작업이 거의 끝나갑니다. 지금까지 Lean이 임의 개수의 재귀적 생성자를 사용하여 귀납적 타입을 도입하는 것을 허용한다는 사실을 보았습니다. 사실, 하나의 귀납적 정의가 이제부터 설명할 방식으로 귀납적 타입의 인덱싱된 family를 도입할 수도 있습니다.

귀납적 패밀리(inductive family)는 다음과 같은 형태의 동시 귀납법으로 정의되는 색인화된 타입들의 패밀리입니다.

inductive foo : ... → Sort u where
  | constructor₁ : ... → foo ...
  | constructor₂ : ... → foo ...
  ...
  | constructorₙ : ... → foo ...

Sort u의 원소를 구성하는 일반적인 귀납적 정의와 달리, 더 일반화된 버전은 함수 ... → Sort u를 구성하며, 여기서 “...”는 인자 타입들의 나열을 나타내고, 이는 indices라고도 불립니다. 각 생성자는 이 패밀리의 어떤 멤버의 원소를 구성합니다. 한 가지 예로는 Vect α n의 정의가 있는데, 이는 길이가 nα의 원소로 이루어진 벡터의 타입입니다:

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

cons 생성자가 Vect α n의 원소를 받아 Vect α (n + 1)의 원소를 반환함으로써, 해당 계열의 한 구성원의 원소를 사용하여 다른 구성원의 원소를 만들어낸다는 점에 주목하십시오.

더 특이한 예시는 Lean에서의 동치 타입 정의로 주어집니다:

inductive Eq {α : Sort u} (a : α) : α Prop where | refl : Eq a a

고정된 각 α : Sort ua : α에 대해, 이 정의는 x : α로 색인된 타입들의 모임 Eq a x를 구성합니다. 그러나 특기할 점은 생성자가 refl 하나뿐이며, 이는 Eq a a의 원소라는 것입니다. 직관적으로, Eq a x의 증명을 구성하는 유일한 방법은 xa인 경우에 반사성을 사용하는 것입니다. Eq a a는 타입들의 모임 Eq a x 중에서 유일하게 원소를 가지는 타입임에 유의하십시오. Lean이 생성하는 소거 원리는 다음과 같습니다:

universe u v @Eq.rec : {α : Sort u} {a : α} {motive : (a_1 : α) a = a_1 Sort v} motive a (Eq.refl a) {a_1 : α} (t : a = a_1) motive a_1 t#check (@Eq.rec : {α : Sort u} {a : α} {motive : (x : α) a = x Sort v} motive a rfl {b : α} (h : a = b) motive b h)
@Eq.rec : {α : Sort u} 
  {a : α}  {motive : (a_1 : α)  a = a_1  Sort v}  motive a (Eq.refl a)  {a_1 : α}  (t : a = a_1)  motive a_1 t

동치에 대한 모든 기본 공리가 생성자 refl과 소거자 Eq.rec로부터 도출된다는 것은 주목할 만한 사실입니다. 그러나 동치의 정의는 이례적입니다. 공리적 세부 사항에서의 논의를 참고하십시오.

재귀자 Eq.rec는 치환을 정의하는 데에도 사용됩니다:

theorem subst {α : Type u} {a b : α} {p : α Prop} (h₁ : Eq a b) (h₂ : p a) : p b := Eq.rec (motive := fun x _ => p x) h₂ h₁

match를 사용해 subst를 정의할 수도 있습니다.

theorem subst {α : Type u} {a b : α} {p : α Prop} (h₁ : Eq a b) (h₂ : p a) : p b := match h₁ with | rfl => h₂

실제로 Lean은 Eq.casesOn, Eq.ndrec와 같이 생성된 도우미들을 기반으로 한 정의를 사용하여 match 표현식을 컴파일하며, 이 도우미들 자체는 Eq.rec를 사용하여 정의됩니다.

theorem subst {α : Type u} {a b : α} {p : α Prop} (h₁ : a = b) (h₂ : p a) : p b := match h₁ with | rfl => h₂ set_option pp.all true theorem subst.{u} : {α : Type u} {a b : α} {p : α Prop} (h₁ : @Eq.{u + 1} α a b) (h₂ : p a), p b := fun {α : Type u} {a b : α} {p : α Prop} (h₁ : @Eq.{u + 1} α a b) (h₂ : p a) => @subst.match_1_1.{u} α a (fun (b : α) (h₁ : @Eq.{u + 1} α a b) => p b) b h₁ fun (_ : Unit) => h₂#print subst
theorem subst.{u} :  {α : Type u} {a b : α} {p : α  Prop} (h₁ : @Eq.{u + 1} α a b) (h₂ : p a), p b :=
fun {α : Type u} {a b : α} {p : α  Prop} (h₁ : @Eq.{u + 1} α a b) (h₂ : p a) =>
  @subst.match_1_1.{u} α a (fun (b : α) (h₁ : @Eq.{u + 1} α a b) => p b) b h₁ fun (_ : Unit) => h₂
@[instance_reducible] def subst.match_1_1.{u_1} : {α : Type u_1} {a : α} (motive : (b : α) @Eq.{u_1 + 1} α a b Prop) (b : α) (h₁ : @Eq.{u_1 + 1} α a b) (h_1 : (_ : Unit), motive a (@Eq.refl.{u_1 + 1} α a)), motive b h₁ := fun {α : Type u_1} {a : α} (motive : (b : α) @Eq.{u_1 + 1} α a b Prop) (b : α) (h₁ : @Eq.{u_1 + 1} α a b) (h_1 : (_ : Unit), motive a (@Eq.refl.{u_1 + 1} α a)) => @Eq.casesOn.{0, u_1 + 1} α a (fun (a_1 : α) (x : @Eq.{u_1 + 1} α a a_1) => (h : @Eq.{u_1 + 1} α b a_1) (h : @HEq.{0} (@Eq.{u_1 + 1} α a b) h₁ (@Eq.{u_1 + 1} α a a_1) x), motive b h₁) b h₁ (fun (h : @Eq.{u_1 + 1} α b a) => @Eq.ndrec.{0, u_1 + 1} α a (fun (b : α) => (h₁ : @Eq.{u_1 + 1} α a b) (h : @HEq.{0} (@Eq.{u_1 + 1} α a b) h₁ (@Eq.{u_1 + 1} α a a) (@Eq.refl.{u_1 + 1} α a)), motive b h₁) (fun (h₁ : @Eq.{u_1 + 1} α a a) (h : @HEq.{0} (@Eq.{u_1 + 1} α a a) h₁ (@Eq.{u_1 + 1} α a a) (@Eq.refl.{u_1 + 1} α a)) => @Eq.ndrec.{0, 0} (@Eq.{u_1 + 1} α a a) (@Eq.refl.{u_1 + 1} α a) (fun (h₁ : @Eq.{u_1 + 1} α a a) => motive a h₁) (h_1 Unit.unit) h₁ (@Eq.symm.{0} (@Eq.{u_1 + 1} α a a) h₁ (@Eq.refl.{u_1 + 1} α a) (@eq_of_heq.{0} (@Eq.{u_1 + 1} α a a) h₁ (@Eq.refl.{u_1 + 1} α a) h))) b (@Eq.symm.{u_1 + 1} α b a h) h₁) (@Eq.refl.{u_1 + 1} α b) (@HEq.refl.{0} (@Eq.{u_1 + 1} α a b) h₁)#print subst.match_1_1
@[instance_reducible] def subst.match_1_1.{u_1} :  {α : Type u_1} {a : α}
  (motive : (b : α)  @Eq.{u_1 + 1} α a b  Prop) (b : α) (h₁ : @Eq.{u_1 + 1} α a b)
  (h_1 :  (_ : Unit), motive a (@Eq.refl.{u_1 + 1} α a)), motive b h₁ :=
fun {α : Type u_1} {a : α} (motive : (b : α)  @Eq.{u_1 + 1} α a b  Prop) (b : α) (h₁ : @Eq.{u_1 + 1} α a b)
    (h_1 :  (_ : Unit), motive a (@Eq.refl.{u_1 + 1} α a)) =>
  @Eq.casesOn.{0, u_1 + 1} α a
    (fun (a_1 : α) (x : @Eq.{u_1 + 1} α a a_1) =>
       (h : @Eq.{u_1 + 1} α b a_1) (h : @HEq.{0} (@Eq.{u_1 + 1} α a b) h₁ (@Eq.{u_1 + 1} α a a_1) x), motive b h₁)
    b h₁
    (fun (h : @Eq.{u_1 + 1} α b a) =>
      @Eq.ndrec.{0, u_1 + 1} α a
        (fun (b : α) =>
           (h₁ : @Eq.{u_1 + 1} α a b)
            (h : @HEq.{0} (@Eq.{u_1 + 1} α a b) h₁ (@Eq.{u_1 + 1} α a a) (@Eq.refl.{u_1 + 1} α a)), motive b h₁)
        (fun (h₁ : @Eq.{u_1 + 1} α a a)
            (h : @HEq.{0} (@Eq.{u_1 + 1} α a a) h₁ (@Eq.{u_1 + 1} α a a) (@Eq.refl.{u_1 + 1} α a)) =>
          @Eq.ndrec.{0, 0} (@Eq.{u_1 + 1} α a a) (@Eq.refl.{u_1 + 1} α a)
            (fun (h₁ : @Eq.{u_1 + 1} α a a) => motive a h₁) (h_1 Unit.unit) h₁
            (@Eq.symm.{0} (@Eq.{u_1 + 1} α a a) h₁ (@Eq.refl.{u_1 + 1} α a)
              (@eq_of_heq.{0} (@Eq.{u_1 + 1} α a a) h₁ (@Eq.refl.{u_1 + 1} α a) h)))
        b (@Eq.symm.{u_1 + 1} α b a h) h₁)
    (@Eq.refl.{u_1 + 1} α b) (@HEq.refl.{0} (@Eq.{u_1 + 1} α a b) h₁)
@[reducible] def Eq.casesOn.{u, u_1} : {α : Sort u_1} {a : α} {motive : (a_1 : α) (t : @Eq.{u_1} α a a_1) Sort u} {a_1 : α} (t : @Eq.{u_1} α a a_1) (refl : motive a (@Eq.refl.{u_1} α a)) motive a_1 t := fun {α : Sort u_1} {a : α} {motive : (a_1 : α) (t : @Eq.{u_1} α a a_1) Sort u} {a_1 : α} (t : @Eq.{u_1} α a a_1) (refl : motive a (@Eq.refl.{u_1} α a)) => @Eq.rec.{u, u_1} α a motive refl a_1 t#print Eq.casesOn
@[reducible] def Eq.casesOn.{u, u_1} : {α : Sort u_1} 
  {a : α} 
    {motive : (a_1 : α)  (t : @Eq.{u_1} α a a_1)  Sort u} 
      {a_1 : α}  (t : @Eq.{u_1} α a a_1)  (refl : motive a (@Eq.refl.{u_1} α a))  motive a_1 t :=
fun {α : Sort u_1} {a : α} {motive : (a_1 : α)  (t : @Eq.{u_1} α a a_1)  Sort u} {a_1 : α} (t : @Eq.{u_1} α a a_1)
    (refl : motive a (@Eq.refl.{u_1} α a)) =>
  @Eq.rec.{u, u_1} α a motive refl a_1 t
@[reducible] def Eq.ndrec.{u1, u2} : {α : Sort u2} {a : α} {motive : α Sort u1} (m : motive a) {b : α} (h : @Eq.{u2} α a b) motive b := fun {α : Sort u2} {a : α} {motive : α Sort u1} (m : motive a) {b : α} (h : @Eq.{u2} α a b) => @Eq.rec.{u1, u2} α a (fun (x : α) (x_1 : @Eq.{u2} α a x) => motive x) m b h#print Eq.ndrec
@[reducible] def Eq.ndrec.{u1, u2} : {α : Sort u2} 
  {a : α}  {motive : α  Sort u1}  (m : motive a)  {b : α}  (h : @Eq.{u2} α a b)  motive b :=
fun {α : Sort u2} {a : α} {motive : α  Sort u1} (m : motive a) {b : α} (h : @Eq.{u2} α a b) =>
  @Eq.rec.{u1, u2} α a (fun (x : α) (x_1 : @Eq.{u2} α a x) => motive x) m b h

리커서(recursor)나 h₁ : a = b를 사용하는 match를 이용하면, ab가 같다고 가정할 수 있으며, 이 경우 p bp a는 같습니다.

Eq가 대칭적이고 추이적이라는 것을 증명하기는 어렵지 않습니다. 다음 예제에서는 symm을 증명하고, transcongr(합동) 정리는 연습 문제로 남겨 둡니다.

variable {α β : Type u} {a b c : α} theorem symm (h : Eq a b) : Eq b a := match h with | rfl => rfl theorem declaration uses `sorry`trans (h₁ : Eq a b) (h₂ : Eq b c) : Eq a c := sorry theorem declaration uses `sorry`congr (f : α β) (h : Eq a b) : Eq (f a) (f b) := sorry

타입 이론 문헌에는 귀납적 정의를 더욱 일반화한 원리들이 존재하는데, 예를 들어 induction-recursioninduction-induction이 그것입니다. 이들은 Lean에서 지원되지 않습니다.

7.8. 공리적 세부 사항🔗

지금까지 예시를 통해 귀납적 타입과 그 문법을 설명했습니다. 이 절에서는 공리적 기반에 관심 있는 독자를 위해 추가 정보를 제공합니다.

귀납적 타입의 생성자는 매개변수—직관적으로 귀납적 구성 전체에 걸쳐 고정된 채로 남아 있는 인자—와 인덱스, 즉 동시에 구성 중인 타입 모임을 매개변수화하는 인자를 받는다는 것을 우리는 살펴보았습니다. 각 생성자는 타입을 가져야 하며, 이때 인자 타입은 이전에 정의된 타입들과 매개변수 및 인덱스 타입들, 그리고 현재 정의 중인 귀납적 타입 모임으로부터 구성됩니다. 요구 사항은, 후자가 조금이라도 존재한다면 그것이 오직 엄격하게 양의 위치에서만 나타난다는 것입니다. 이는 단순히, 그것이 나타나는 생성자의 모든 인자가 의존 화살표 타입이며, 이 타입에서 정의 중인 귀납적 타입은 오직 결과 타입으로만 나타나고, 이때 인덱스는 상수와 이전 인자들을 이용해 주어진다는 것을 의미합니다.

귀납적 타입은 어떤 u에 대해 Sort u에 존재하므로, u어떤 유니버스 수준으로 인스턴스화될 수 있는지 묻는 것이 합당합니다. 귀납적 타입들의 모임 C의 정의에 있는 각 생성자 c는 다음과 같은 형태입니다.

  c : (a : α) → (b : β[a]) → C a p[a,b]

여기서 a는 데이터 타입 매개변수의 시퀀스이고, b는 생성자에 대한 인자의 시퀀스이며, p[a, b]는 이 구성이 귀속되는 귀납적 패밀리의 원소를 결정하는 인덱스입니다. (이 설명은 다소 오해의 소지가 있는데, 생성자에 대한 인자는 의존성이 성립하는 한 어떤 순서로도 나타날 수 있기 때문입니다.) C의 유니버스 레벨에 대한 제약은 귀납적 타입이 Prop(즉 Sort 0)에 속하도록 지정되었는지 여부에 따라 두 가지 경우로 나뉩니다.

먼저 귀납적 타입이 Prop에 속하는 것으로 지정되지 않은 경우를 살펴봅시다. 그러면 유니버스 수준 u는 다음을 만족하도록 제약됩니다:

위와 같은 각 생성자 c에 대해, 그리고 시퀀스 β[a]의 각 βk[a]에 대해, βk[a] : Sort v이면, uv가 성립합니다.

다시 말해, 유니버스 레벨 u는 생성자의 인자를 나타내는 각 타입의 유니버스 레벨보다 크거나 같아야 합니다.

귀납적 타입이 Prop에 속하도록 지정된 경우, 생성자 인자의 유니버스 수준에는 아무런 제약이 없습니다. 하지만 이러한 유니버스 수준은 소거 규칙에 영향을 미칩니다. 일반적으로 Prop에 속하는 귀납적 타입의 경우, 소거 규칙의 모티브는 Prop에 속해야 합니다.

이 마지막 규칙에는 예외가 하나 있습니다. 생성자가 단 하나뿐이고 각 생성자 인자가 Prop에 속하거나 인덱스인 경우, 귀납적으로 정의된 Prop으로부터 임의의 Sort로 소거하는 것이 허용됩니다. 이러한 직관의 근거는, 이 경우 소거가 해당 인자의 타입이 거주자를 갖는다는 단순한 사실만으로 이미 주어진 것 이외의 어떠한 정보도 사용하지 않는다는 데 있습니다. 이 특수한 경우는 singleton elimination이라고 알려져 있습니다.

귀납적으로 정의된 동치 타입의 소거자인 Eq.rec의 적용에서 이미 단일 소거(singleton elimination)가 작동하는 것을 보았습니다. p ap b가 임의의 타입인 경우에도 원소 h : Eq a b를 사용해 원소 h₂ : p ap b로 형 변환할 수 있는데, 이는 이 변환이 새로운 데이터를 생성하지 않고 이미 가지고 있는 데이터를 재해석할 뿐이기 때문입니다. 단일 소거는 이종 동치(heterogeneous equality) 및 정초 재귀(well-founded recursion)와 함께도 사용되며, 이에 대해서는 귀납법과 재귀 장에서 다룰 것입니다.

7.9. 상호 재귀적 및 중첩된 귀납적 타입🔗

이제 자주 유용하게 쓰이는 귀납적 타입의 두 가지 일반화를 살펴보겠습니다. Lean은 이를 앞서 설명한 더 원시적인 종류의 귀납적 타입으로 “컴파일”함으로써 지원합니다. 다시 말해, Lean은 더 일반화된 정의를 파싱하여 이를 바탕으로 보조 귀납적 타입을 정의하고, 그런 다음 이 보조 타입을 사용하여 우리가 실제로 원하는 타입을 정의합니다. 이러한 타입을 효과적으로 활용하려면 다음 장에서 설명할 Lean의 방정식 컴파일러가 필요합니다. 그럼에도 불구하고, 이러한 선언들은 일반적인 귀납적 정의의 단순한 변형에 지나지 않으므로 여기서 설명하는 것이 타당합니다.

첫째, Lean은 상호 정의된(mutually defined) 귀납적 타입을 지원합니다. 여기서 아이디어는 두 개(또는 그 이상)의 귀납적 타입을 동시에 정의할 수 있으며, 각각이 서로를 참조할 수 있다는 것입니다.

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

이 예제에서는 두 타입이 동시에 정의됩니다. 자연수 n0이거나 Odd인 수보다 1 크면 Even이고, Even인 수보다 1 크면 Odd입니다. 아래 연습 문제에서는 세부 사항을 직접 풀어써 보도록 요청합니다.

상호 귀납적 정의를 사용하여 α의 원소로 레이블이 붙은 유한 트리의 표기법을 정의할 수도 있습니다:

mutual inductive Tree (α : Type u) where | node : α TreeList α Tree α inductive TreeList (α : Type u) where | nil : TreeList α | cons : Tree α TreeList α TreeList α end

이 정의를 사용하면, α의 원소와 (비어 있을 수도 있는) 서브트리 리스트를 함께 제공하여 Tree α의 원소를 구성할 수 있습니다. 서브트리 리스트는 TreeList α 타입으로 표현되며, 이 타입은 빈 리스트인 nil이거나, 트리와 TreeList α의 원소로 이루어진 cons 중 하나로 정의됩니다.

하지만 이 정의는 다루기에 불편합니다. 서브트리 목록이 List (Tree α) 타입으로 주어진다면 훨씬 더 좋을 것입니다. 특히 Lean의 라이브러리에는 목록을 다루기 위한 수많은 함수와 정리가 포함되어 있기 때문입니다. TreeList α 타입이 List (Tree α)isomorphic함을 보일 수 있지만, 이 동형 사상을 따라 결과를 이리저리 변환하는 작업은 지루합니다.

사실 Lean에서는 우리가 정말로 원하는 귀납적 타입을 정의할 수 있습니다:

inductive Tree (α : Type u) where | mk : α List (Tree α) Tree α

이를 중첩된(nested) 귀납적 타입이라고 합니다. Treemk의 인자들 사이에서 엄격하게 양의 위치에 나타나지 않고, 오히려 List 타입 생성자 내부에 중첩되어 있기 때문에, 이는 이전 절에서 제시한 귀납적 타입의 엄격한 명세를 벗어납니다. 그러면 Lean은 커널에서 TreeList αList (Tree α) 사이의 동형사상을 자동으로 구성하고, 이 동형사상을 바탕으로 Tree의 생성자들을 정의합니다.

7.10. 연습문제🔗

  1. 곱셈, 전임자 함수(pred 0 = 0을 만족하는), 절단 뺄셈(mn보다 크거나 같을 때 n - m = 0을 만족하는), 거듭제곱 등 자연수에 대한 다른 연산들을 정의해 보십시오. 그런 다음 이미 증명한 정리들을 바탕으로 이러한 연산들의 기본적인 성질 중 일부를 증명해 보십시오.

    이 중 다수는 이미 Lean의 코어 라이브러리에 정의되어 있으므로, 이름 충돌을 피하기 위해 Hidden이나 그와 비슷한 이름의 네임스페이스 안에서 작업해야 합니다.

  2. length 함수나 reverse 함수와 같은 리스트 연산을 몇 가지 정의하십시오. 다음과 같은 성질들을 증명하십시오:

    a. length (xs ++ ys) = length xs + length ys

    b. length (reverse xs) = length xs

    c. reverse (reverse xs) = xs

  3. 다음 생성자로부터 만들어지는 항으로 구성된 귀납적 데이터 타입을 정의하십시오:

    • const n, 자연수 n을 나타내는 상수

    • var n, 즉 번호가 n인 변수

    • plus s t, st의 합을 나타냅니다

    • times s t, st의 곱을 나타냅니다

    변수에 값을 할당한 것에 대해 그러한 항을 평가하는 함수를 재귀적으로 정의하십시오.

  4. 마찬가지로, 명제 논리식의 타입을 정의하고, 이러한 논리식 타입에 대한 함수들, 즉 평가 함수, 논리식의 복잡도를 측정하는 함수들, 그리고 주어진 변수를 다른 논리식으로 치환하는 함수도 정의하십시오.