우리는 Lean의 형식적 기초가 기본 타입인 Prop, Type0, Type1, Type2, ...를 포함하며, 의존 함수 타입인 (x:α)→β의 구성을 허용한다는 것을 살펴보았습니다. 예제에서는 Bool, Nat, Int와 같은 추가적인 타입과, List와 같은 타입 생성자, 그리고 곱 ×도 사용했습니다. 실제로 Lean의 라이브러리에서는, 유니버스를 제외한 모든 구체적인 타입과 의존 화살표를 제외한 모든 타입 생성자가 귀납적 타입이라고 알려진 타입 구성의 일반적인 계열의 인스턴스입니다. 타입 유니버스, 의존 화살표 타입, 귀납적 타입만으로도 상당한 규모의 수학 체계를 구축할 수 있다는 것은 주목할 만하며, 그 외의 모든 것은 이들로부터 따라 나옵니다.
직관적으로, 귀납적 타입은 지정된 생성자 목록으로부터 구축됩니다. Lean에서 이러한 타입을 지정하는 구문은 다음과 같습니다:
직관적으로 말해, 각 생성자는 이전에 생성된 값으로부터 새로운 Foo 객체를 만드는 방법을 명시합니다. Foo 타입은 이러한 방식으로 생성된 객체 이외에는 아무것도 포함하지 않습니다.
아래에서 살펴보겠지만, 생성자의 인자는 특정 “양성(positivity)” 제약을 따르는 한 Foo 타입의 객체를 포함할 수 있으며, 이 제약은 Foo의 원소가 아래에서부터 위로 구축됨을 보장합니다. 대략적으로 말하면, 각 ...는 Foo와 이전에 정의된 타입들로부터 구성된 화살표 타입이라면 무엇이든 될 수 있으며, 여기서 Foo가 등장한다면 그것은 오직 의존 화살표 타입의 “대상(target)”으로서만 등장할 수 있습니다.
귀납적 타입의 예시를 여럿 제시하겠습니다. 또한 위 도식을 상호 정의된 귀납적 타입과 이른바 귀납적 패밀리로 약간 일반화하는 것도 살펴보겠습니다.
논리 연결사와 마찬가지로, 모든 귀납적 타입에는 해당 타입의 원소를 구성하는 방법을 보여 주는 도입 규칙과, 해당 타입의 원소를 다른 구성에서 “사용”하는 방법을 보여 주는 소거 규칙이 함께 옵니다. 논리 연결사와의 유비는 놀라운 일이 아닙니다. 아래에서 보게 되겠지만, 이들 역시 귀납적 타입 구성의 예이기 때문입니다. 귀납적 타입에 대한 도입 규칙은 이미 본 적이 있는데, 이는 바로 해당 타입의 정의에서 명시된 생성자들입니다. 소거 규칙은 해당 타입에 대한 재귀 원리를 제공하며, 이는 특수한 경우로서 귀납법 원리도 포함합니다.
다음 장에서는 Lean의 함수 정의 패키지에 대해 설명하는데, 이는 귀납적 타입에 대한 함수를 정의하고 귀납적 증명을 수행하는 더욱 편리한 방법을 제공합니다. 하지만 귀납적 타입이라는 개념이 매우 근본적이기 때문에, 저수준의 실습적인 이해에서부터 시작하는 것이 중요하다고 생각합니다. 귀납적 타입의 몇 가지 기본적인 예제로 시작하여, 점차 더 정교하고 복잡한 예제로 나아가겠습니다.
sunday, monday, ... , saturday를 다른 구별되는 특성이 전혀 없는, Weekday의 서로 다른 원소로 생각하십시오. 소거 원리인 Weekday.rec는 타입 Weekday와 그 생성자들과 함께 정의됩니다. 이는 재귀자라고도 알려져 있으며, 타입을 “귀납적”으로 만드는 것이 바로 이것입니다: 각 생성자에 대응하는 값을 지정함으로써 Weekday에 대한 함수를 정의할 수 있게 해줍니다. 직관적으로 귀납적 타입은 생성자들에 의해 남김없이 생성되며, 그것들이 구성하는 것 이외의 원소는 갖지 않습니다.
Lean의 논리를 사용할 때, match 표현식은 귀납적 타입을 선언할 때 생성되는 재귀자(recursor)Weekday.rec를 사용하여 컴파일됩니다. 이는 결과로 나오는 항이 타입 이론에서 잘 정의되도록 보장합니다. 컴파일된 코드의 경우, match는 다른 함수형 프로그래밍 언어에서와 마찬가지로 컴파일됩니다.
귀납적 데이터 타입을 선언할 때, derivingRepr을 사용하여 Weekday 객체를 텍스트로 변환하는 함수를 생성하도록 Lean에 지시할 수 있습니다. 이 함수는 #eval 명령이 Weekday 객체를 표시하는 데 사용됩니다. Repr이 존재하지 않으면, #eval은 그 자리에서 하나를 유도하려고 시도합니다.
아래의 귀납적 타입을 위한 택틱에서는 귀납적 타입을 활용하도록 특별히 설계된 추가 택틱들을 소개합니다.
propositions-as-types 대응 관계에서, 함수를 정의할 때뿐만 아니라 정리를 증명할 때도 match를 사용할 수 있다는 점에 주목하십시오. 다시 말해, propositions-as-types 대응 관계에서 경우에 따른 증명은 일종의 경우에 따른 정의이며, 여기서 “정의”되는 것은 데이터 조각이 아니라 증명입니다.
(이 예제들을 실행하기 위해, Bool과 같은 이름이 표준 라이브러리의 Bool과 충돌하지 않도록 이들을 Hidden이라는 네임스페이스에 넣었습니다. 이는 이러한 타입들이 시스템이 시작될 때 자동으로 임포트되는 Lean “프렐루드”의 일부이기 때문에 필요합니다.)
연습 문제로서, 이 타입들에 대한 도입 규칙과 소거 규칙이 무엇을 하는지 생각해 보십시오. 추가 연습 문제로, Bool 타입에 불리언 연산 and, or, not을 정의하고 일반적인 항등식을 검증해 볼 것을 제안합니다. and와 같은 이항 연산은 match를 사용해서 정의할 수 있다는 점에 유의하십시오.
이 예제들에서 무슨 일이 일어나고 있는지 살펴봅시다. 곱 타입에는 두 개의 인자를 받는 생성자 Prod.mk 하나가 있습니다. Prodαβ에 대한 함수를 정의하려면, 입력이 Prod.mkab 형태라고 가정할 수 있으며, a와 b를 이용하여 출력을 명시해야 합니다. 이를 이용하여 Prod에 대한 두 개의 사영을 정의할 수 있습니다. 표준 라이브러리는 Prodαβ에 대해 α×β 표기법을, Prod.mkab에 대해 (a,b) 표기법을 정의한다는 점을 기억하십시오.
deffst{α:Typeu}{β:Typev}(p:Prodαβ):α:=matchpwith|Prod.mkaVariable 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]_bNote: This linter can be disabled with `set_option linter.unusedVariables false`b=>adefsnd{α:Typeu}{β:Typev}(p:Prodαβ):β:=matchpwith|Prod.mkVariable 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]_aNote: This linter can be disabled with `set_option linter.unusedVariables false`ab=>b
endHidden
함수 fst는 순서쌍 p를 인자로 받습니다. match는 p를 순서쌍 Prod.mkab로 해석합니다. 또한 의존 타입 이론에서 살펴보았듯이, 이 정의들에 가능한 한 최대의 일반성을 부여하기 위해 타입 α와 β가 임의의 유니버스에 속하도록 허용한다는 점을 상기하십시오.
인자 motive는 구성하려는 객체의 타입을 지정하는 데 사용되며, 이는 쌍(pair)에 의존할 수 있기 때문에 함수입니다. cond 함수는 불리언 조건문으로, condbt1t2는 b가 참이면 t1을, 그렇지 않으면 t2를 반환합니다. 함수 prod_example은 불리언 b와 숫자 n으로 이루어진 쌍을 받아, b가 참인지 거짓인지에 따라 2*n 또는 2*n+1을 반환합니다.
반면, 합 타입은 inl과 inr(각각 “왼쪽 삽입”과 “오른쪽 삽입”을 뜻함)이라는 두 개의 생성자를 가지며, 이들 각각은 하나의 (명시적) 인자를 받습니다. Sumαβ에 대한 함수를 정의하려면 두 가지 경우를 처리해야 합니다: 입력이 inla 형태이면 a를 이용해 출력 값을 지정해야 하고, 입력이 inrb 형태이면 b를 이용해 출력 값을 지정해야 합니다.
이 예제는 이전 예제와 유사하지만, 이번에는 sum_example에 대한 입력이 암묵적으로 inln 또는 inrn 형태 중 하나입니다. 첫 번째 경우 함수는 2*n을 반환하고, 두 번째 경우에는 2*n+1을 반환합니다.
곱 타입이 Prod뿐만 아니라 생성자에도 인자로 쓰이는 매개변수 αβ : Type에 의존한다는 점에 주목하십시오. Lean은 이러한 인자가 생성자의 이후 인자나 반환 타입으로부터 추론될 수 있는 경우를 감지하며, 그러한 경우에는 이들을 암묵적 인자로 만듭니다.
자연수 정의하기에서는 귀납적 타입의 생성자가 그 귀납적 타입 자체로부터 인자를 취할 때 어떤 일이 일어나는지 살펴보겠습니다. 이 절에서 다루는 예제들의 특징은 각 생성자가 이전에 지정된 타입에만 의존한다는 점입니다.
생성자가 여러 개인 타입은 선언적임에 유의하십시오. Sumαβ의 원소는 inla 형태이거나 또는inlb 형태입니다. 인수가 여러 개인 생성자는 결합적 정보를 도입합니다. Prodαβ의 원소 Prod.mkab로부터 a그리고b를 추출할 수 있습니다. 임의의 귀납적 타입은 원하는 개수의 생성자를 가지고, 각 생성자가 원하는 개수의 인수를 취함으로써 두 특성을 모두 포함할 수 있습니다.
함수 정의에서와 마찬가지로, Lean의 귀납적 정의 문법에서도 생성자의 이름 있는 인자를 콜론 앞에 둘 수 있습니다.
Prod와 같이 생성자가 단 하나뿐인 타입은 순수하게 논리곱적입니다. 즉, 생성자는 단순히 인자 목록을 하나의 데이터로 묶으며, 이는 본질적으로 이후 인자의 타입이 앞선 인자의 타입에 의존할 수 있는 튜플입니다. 이러한 타입은 “레코드” 또는 “구조체”로도 생각할 수 있습니다. Lean에서는 structure 키워드를 사용하여 이러한 귀납적 타입과 그 사영을 동시에 정의할 수 있습니다.
의존 타입 이론의 의미론에는 부분 함수라는 내장된 개념이 없습니다. 함수 타입 α→β나 의존 함수 타입 (a:α)→β의 모든 원소는 모든 입력에 대해 값을 갖는다고 가정됩니다. Option 타입은 부분 함수를 표현하는 방법을 제공합니다. Optionβ의 원소는 none이거나, 어떤 값 b : β에 대해 someb 형태입니다. 따라서 타입 α→Optionβ의 원소 f를 α에서 β로 가는 부분 함수로 생각할 수 있습니다. 모든 a : α에 대해, fa는 fa가 “정의되지 않음”을 나타내는 none을 반환하거나, someb를 반환합니다.
Inhabitedα의 원소는 단순히 α의 원소가 존재한다는 사실에 대한 증거입니다. 나중에 우리는 Inhabited가 Lean에서 타입 클래스의 한 예시임을 보게 될 것입니다: Lean에게 적절한 기본 타입들이 거주된다는 것을 알려줄 수 있으며, 이를 바탕으로 다른 구성된 타입들이 거주된다는 것을 자동으로 추론할 수 있습니다.
연습 문제로서, α에서 β로, 그리고 β에서 γ로 가는 부분 함수의 합성 개념을 개발하고, 그것이 예상대로 동작함을 보이시기 바랍니다. 또한 Bool과 Nat이 거주됨을, 두 거주된 타입의 곱이 거주됨을, 그리고 거주된 타입으로 가는 함수의 타입이 거주됨을 보이시기 바랍니다.
이러한 것들이 여러분이 이미 살펴본 도입 규칙과 소거 규칙을 어떻게 발생시키는지 생각해 보아야 합니다. 귀납적 타입의 소거자가 어디로 소거할 수 있는지, 즉 어떤 종류의 타입이 재귀자의 대상이 될 수 있는지를 지배하는 규칙들이 있습니다. 대략적으로 말하면, Prop에 속한 귀납적 타입을 특징짓는 것은 오직 Prop에 속한 다른 타입으로만 소거할 수 있다는 점입니다. 이는 p : Prop일 때 원소 hp : p가 어떤 데이터도 담지 않는다는 이해와 일치합니다. 하지만 이 규칙에는 작은 예외가 하나 있는데, 이는 아래 귀납적 패밀리에서 다루겠습니다.
False, True, And, Or의 정의는 Empty, Unit, Prod, Sum의 정의와 완벽하게 유사합니다. 차이점은 첫 번째 그룹은 Prop의 원소를 산출하고, 두 번째 그룹은 어떤 u에 대해 Typeu의 원소를 산출한다는 것입니다. 이와 유사한 방식으로, ∃x:α,p는 Σx:α,β의 Prop 값 변형입니다.
여기서 {x:α//p}로 표시되는 또 다른 귀납적 타입을 언급하는 것이 좋겠습니다. 이는 ∃x:α,p와 Σx:α,β의 일종의 혼합체입니다.
지금까지 살펴본 귀납적으로 정의된 타입들은 “평평”합니다: 생성자는 데이터를 감싸서 타입에 삽입하며, 그에 대응하는 재귀자는 데이터를 풀어서 그것에 대해 작동합니다. 생성자가 정의되고 있는 바로 그 타입의 원소들에 대해 작동할 때 상황은 훨씬 더 흥미로워집니다. 자연수 타입 Nat이 대표적인 예입니다:
생성자는 두 개가 있습니다. 먼저 zero : Nat부터 시작하는데, 이는 인자를 받지 않으므로 처음부터 가지고 있는 셈입니다. 이와 대조적으로, 생성자 succ은 이전에 구성된 Nat에만 적용할 수 있습니다. 이를 zero에 적용하면 succzero:Nat이 만들어집니다. 이를 다시 적용하면 succ(succzero):Nat이 만들어지며, 이런 식으로 계속됩니다. 직관적으로 Nat은 이러한 생성자들을 갖는 “가장 작은” 타입이며, 이는 zero에서 시작하여 succ을 반복적으로 적용함으로써 남김없이 (그리고 자유롭게) 생성됨을 의미합니다.
이전과 마찬가지로, Nat에 대한 재귀자는 Nat에서 임의의 정의역으로 향하는 의존 함수 f, 즉 어떤 motive : Nat→Sortu에 대해 (n:Nat)→motiven의 원소인 f를 정의하도록 설계되어 있습니다. 이 재귀자는 두 가지 경우를 처리해야 하는데, 입력이 zero인 경우와 입력이 어떤 n : Nat에 대해 succn 형태인 경우입니다. 첫 번째 경우에는, 이전과 마찬가지로 알맞은 타입을 갖는 목표 값을 그냥 지정합니다. 그러나 두 번째 경우에는, 재귀자가 n에서의 f의 값이 이미 계산되어 있다고 가정할 수 있습니다. 그 결과, 재귀자의 다음 인자는 n과 fn을 이용하여 f(succn)의 값을 지정합니다. 재귀자의 타입을 확인해 보면 다음과 같습니다.
암시적 인자인 motive는 정의되는 함수의 공역입니다. 타입 이론에서는 motive가 소거/재귀의 동기라고 흔히 말하는데, 이는 우리가 구성하고자 하는 대상의 종류를 나타내기 때문입니다. 다음 두 인자는 위에서 설명한 것처럼 0의 경우와 후행자의 경우를 계산하는 방법을 명시합니다. 이들은 부전제라고도 알려져 있습니다. 마지막으로, t:Nat은 함수의 입력입니다. 이는 주전제라고도 알려져 있습니다.
Nat.recOn은 Nat.rec과 유사하지만, 주요 전제가 부차 전제들보다 앞에 옵니다.
예를 들어, 자연수에 대한 덧셈 함수 addmn을 생각해 봅시다. m을 고정하면, n에 대한 재귀법으로 덧셈을 정의할 수 있습니다. 기본 단계에서는 addmzero를 m으로 둡니다. 후행자 단계에서는, addmn의 값이 이미 결정되어 있다고 가정하고, addm(succn)을 succ(addmn)으로 정의합니다.
inductiveNatwhere|zero:Nat|succ:Nat→NatderivingReprdefadd(mn:Nat):Nat:=matchnwith|Nat.zero=>m|Nat.succn=>Nat.succ(addmn)openAmbiguous 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`NatHidden.Nat.succ (Hidden.Nat.succ (Hidden.Nat.succ (Hidden.Nat.zero)))#evaladd(succ(succzero))(succzero)
instance 명령이 어떻게 작동하는지는 타입 클래스 장에서 설명하겠습니다. 아래 예제에서는 Lean 버전의 자연수를 사용하겠습니다.
하지만 0+n=n과 같은 사실을 증명하려면 귀납법에 의한 증명이 필요합니다. 앞서 살펴본 바와 같이, 공역 motiven이 Prop의 원소인 경우 귀납법 원리는 재귀 원리의 특수한 경우일 뿐입니다. 이는 귀납적 증명의 익숙한 패턴을 나타냅니다: ∀n,motiven을 증명하려면 먼저 motive0을 증명하고, 그다음 임의의 n에 대해 ih : motiven을 가정하고 motive(n+1)을 증명합니다.
다시 한번 주목할 점은, 증명의 맥락에서 Nat.recOn이 사용될 때 이는 실제로 귀납법 원리를 변장한 것에 지나지 않는다는 것입니다. rw와 simp 택틱은 이러한 증명에서 매우 효과적인 경향이 있습니다. 이 경우 각 택틱을 사용하여 증명을 다음과 같이 축소할 수 있습니다:
openNattheoremzero_add(n:Nat):0+n=n:=Nat.recOn(motive:=funx=>0+x=x)nrfl(funnih=>byn✝:Natn:Natih:0+n=n⊢ 0+n.succ=n.succsimp[This simp argument is unused:ihHint: Omit it from the simp argument list.[apply]simpNote: This linter can be disabled with `set_option linter.unusedSimpArgs false`ih]All goals completed! 🐙)
endHidden
다른 예로, 덧셈의 결합법칙 ∀mnk,m+n+k=m+(n+k)을 증명해 봅시다. (우리가 정의한 대로 표기법 +는 왼쪽으로 결합하므로, m+n+k는 실제로는 (m+n)+k입니다.) 가장 어려운 부분은 어떤 변수에 대해 귀납법을 적용할지 알아내는 것입니다. 덧셈은 두 번째 인자에 대한 재귀로 정의되므로 k가 좋은 추측이며, 일단 이 선택을 하고 나면 증명은 거의 저절로 작성됩니다.
openAmbiguous 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`Nattheoremsucc_add(nm:Nat):succn+m=succ(n+m):=Nat.recOn(motive:=funx=>succn+x=succ(n+x))mrfl(funmih=>byn:Natm✝:Natm:Natih:n.succ+m=(n+m).succ⊢ n.succ+m.succ=(n+m.succ).succsimpa[add_succ(succn)]All goals completed! 🐙)theoremadd_comm(mn:Nat):m+n=n+m:=Nat.recOn(motive:=funx=>m+x=x+m)n(bym:Natn:Nat⊢ m+zero=zero+msimp[add_zero,zero_add]All goals completed! 🐙)(funmih=>bym✝:Natn:Natm:Natih:m✝+m=m+m✝⊢ m✝+m.succ=m.succ+m✝simp_all[succ_add,add_succ]All goals completed! 🐙)
Lean에서 귀납적 타입이 지니는 근본적인 중요성을 고려할 때, 이를 효과적으로 다루도록 설계된 다양한 택틱이 존재한다는 사실은 그리 놀라운 일이 아닙니다. 여기서는 그중 일부를 설명합니다.
cases 택틱은 귀납적으로 정의된 타입의 원소에 대해 작동하며, 그 이름이 시사하는 대로 동작합니다. 즉, 가능한 각 생성자에 따라 해당 원소를 분해합니다. 가장 기본적인 형태로는 로컬 컨텍스트에 있는 원소 x에 적용됩니다. 그러면 목표는 x가 각 구성 요소로 대체된 경우들로 축소됩니다.
추가적인 부가 기능들도 있습니다. 한 가지 예로, cases는 with 절을 사용하여 각 경우의 이름을 선택할 수 있게 해줍니다. 예를 들어 다음 예제에서는 succ의 인자에 m이라는 이름을 선택하여, 두 번째 경우가 succm을 참조하도록 합니다. 더 중요한 점은, cases 택틱이 지역 문맥에서 대상 변수에 의존하는 항목들을 감지한다는 것입니다. 이 택틱은 이러한 요소들을 되돌린 뒤, 분할을 수행하고, 다시 도입합니다. 아래 예제에서, 가설 h:n≠0이 첫 번째 분기에서는 h:0≠0이 되고, 두 번째 분기에서는 h:m+1≠0이 되는 것에 주목하십시오.
식 m+3*k가 generalize에 의해 지워진다는 점에 유의하십시오. 중요한 것은 오직 그것이 0 형태인지 아니면 n✝+1 형태인지뿐입니다. 이 형태의 cases는 방정식에서 해당 식(이 경우 m+3*k)을 함께 언급하는 어떤 가설도 되돌리지 않습니다. 만약 그러한 항이 가설에 나타나고 그것도 함께 일반화하고자 한다면, 명시적으로 revert해야 합니다.
케이스 분석하는 표현식이 목표에 나타나지 않으면, cases 택틱은 have를 사용하여 해당 표현식의 타입을 맥락에 넣습니다. 다음은 예시입니다:
정리 Nat.lt_or_gemn은 m<n ∨ m≥n을 말하며, 위 증명을 이 두 경우로 나누는 것으로 생각하는 것이 자연스럽습니다. 첫 번째 분기에서는 가설 hlt:m<n이 있고, 두 번째 분기에서는 가설 hge:m≥n이 있습니다. 위 증명은 다음과 기능적으로 동등합니다.
open Classical을 하면 어떤 명제에 대해서든 배중률을 사용할 수 있다는 점을 기억하십시오. 하지만 타입 클래스 추론(참고: 타입 클래스)을 이용하면 Lean이 실제로 관련 결정 절차를 찾아낼 수 있으며, 이는 계산 가능한 함수에서도 그 경우 분기를 사용할 수 있다는 의미입니다.
cases 택틱을 사용하여 경우 나누기 증명을 수행할 수 있는 것처럼, induction 택틱을 사용하여 귀납법에 의한 증명을 수행할 수 있습니다. 구문은 cases의 구문과 유사하지만, 인자가 지역 문맥의 항일 수만 있다는 점이 다릅니다. 다음은 예시입니다:
openAmbiguous 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`Nattheoremzero_add(n:Nat):0+n=n:=byn:Nat⊢ 0+n=ninductionnzero⊢ 0+zero=zerosucca✝:Nata_ih✝:0+a✝=a✝⊢ 0+a✝.succ=a✝.succ<;>zero⊢ 0+zero=zerosucca✝:Nata_ih✝:0+a✝=a✝⊢ 0+a✝.succ=a✝.succsimp[*,add_zero,add_succ]All goals completed! 🐙theoremsucc_add(mn:Nat):succm+n=succ(m+n):=bym:Natn:Nat⊢ m.succ+n=(m+n).succinductionnzerom:Nat⊢ m.succ+zero=(m+zero).succsuccm:Nata✝:Nata_ih✝:m.succ+a✝=(m+a✝).succ⊢ m.succ+a✝.succ=(m+a✝.succ).succ<;>zerom:Nat⊢ m.succ+zero=(m+zero).succsuccm:Nata✝:Nata_ih✝:m.succ+a✝=(m+a✝).succ⊢ m.succ+a✝.succ=(m+a✝.succ).succsimp[*,add_zero,add_succ]All goals completed! 🐙theoremadd_comm(mn:Nat):m+n=n+m:=bym:Natn:Nat⊢ m+n=n+minductionnzerom:Nat⊢ m+zero=zero+msuccm:Nata✝:Nata_ih✝:m+a✝=a✝+m⊢ m+a✝.succ=a✝.succ+m<;>zerom:Nat⊢ m+zero=zero+msuccm:Nata✝:Nata_ih✝:m+a✝=a✝+m⊢ m+a✝.succ=a✝.succ+msimp[*,add_zero,add_succ,succ_add,zero_add]All goals completed! 🐙theoremadd_assoc(mnk:Nat):m+n+k=m+(n+k):=bym:Natn:Natk:Nat⊢ m+n+k=m+(n+k)inductionkzerom:Natn:Nat⊢ m+n+zero=m+(n+zero)succm:Natn:Nata✝:Nata_ih✝:m+n+a✝=m+(n+a✝)⊢ m+n+a✝.succ=m+(n+a✝.succ)<;>zerom:Natn:Nat⊢ m+n+zero=m+(n+zero)succm:Natn:Nata✝:Nata_ih✝:m+n+a✝=m+(n+a✝)⊢ m+n+a✝.succ=m+(n+a✝.succ)simp[*,add_zero,add_succ]All goals completed! 🐙
endHidden
induction 택틱은 여러 대상(일명 주요 전제)을 가지는 사용자 정의 귀납법 원리도 지원합니다. 이 예제는 Nat.mod.inductionOn을 사용하며, 이는 다음과 같은 시그니처를 가지고 있습니다:
귀납적 타입을 다루는 작업을 용이하게 하기 위해 설계된 마지막 택틱인 injection 택틱으로 이 절을 마무리합니다. 설계상 귀납적 타입의 원소들은 자유롭게 생성되는데, 이는 곧 생성자가 단사적이며 서로소인 치역을 가진다는 것을 의미합니다. injection 택틱은 이 사실을 활용하도록 설계되어 있습니다:
Lean이 허용하는 귀납적 정의의 전체 범위를 설명하는 작업이 거의 끝나갑니다. 지금까지 Lean이 임의 개수의 재귀적 생성자를 사용하여 귀납적 타입을 도입하는 것을 허용한다는 사실을 보았습니다. 사실, 하나의 귀납적 정의가 이제부터 설명할 방식으로 귀납적 타입의 인덱싱된 family를 도입할 수도 있습니다.
귀납적 패밀리(inductive family)는 다음과 같은 형태의 동시 귀납법으로 정의되는 색인화된 타입들의 패밀리입니다.
Sortu의 원소를 구성하는 일반적인 귀납적 정의와 달리, 더 일반화된 버전은 함수 ... → Sortu를 구성하며, 여기서 “...”는 인자 타입들의 나열을 나타내고, 이는 indices라고도 불립니다. 각 생성자는 이 패밀리의 어떤 멤버의 원소를 구성합니다. 한 가지 예로는 Vectαn의 정의가 있는데, 이는 길이가 n인 α의 원소로 이루어진 벡터의 타입입니다:
고정된 각 α:Sortu와 a:α에 대해, 이 정의는 x : α로 색인된 타입들의 모임 Eqax를 구성합니다. 그러나 특기할 점은 생성자가 refl 하나뿐이며, 이는 Eqaa의 원소라는 것입니다. 직관적으로, Eqax의 증명을 구성하는 유일한 방법은 x가 a인 경우에 반사성을 사용하는 것입니다. Eqaa는 타입들의 모임 Eqax 중에서 유일하게 원소를 가지는 타입임에 유의하십시오. Lean이 생성하는 소거 원리는 다음과 같습니다:
지금까지 예시를 통해 귀납적 타입과 그 문법을 설명했습니다. 이 절에서는 공리적 기반에 관심 있는 독자를 위해 추가 정보를 제공합니다.
귀납적 타입의 생성자는 매개변수—직관적으로 귀납적 구성 전체에 걸쳐 고정된 채로 남아 있는 인자—와 인덱스, 즉 동시에 구성 중인 타입 모임을 매개변수화하는 인자를 받는다는 것을 우리는 살펴보았습니다. 각 생성자는 타입을 가져야 하며, 이때 인자 타입은 이전에 정의된 타입들과 매개변수 및 인덱스 타입들, 그리고 현재 정의 중인 귀납적 타입 모임으로부터 구성됩니다. 요구 사항은, 후자가 조금이라도 존재한다면 그것이 오직 엄격하게 양의 위치에서만 나타난다는 것입니다. 이는 단순히, 그것이 나타나는 생성자의 모든 인자가 의존 화살표 타입이며, 이 타입에서 정의 중인 귀납적 타입은 오직 결과 타입으로만 나타나고, 이때 인덱스는 상수와 이전 인자들을 이용해 주어진다는 것을 의미합니다.
귀납적 타입은 어떤 u에 대해 Sortu에 존재하므로, u가 어떤 유니버스 수준으로 인스턴스화될 수 있는지 묻는 것이 합당합니다. 귀납적 타입들의 모임 C의 정의에 있는 각 생성자 c는 다음과 같은 형태입니다.
c : (a : α) → (b : β[a]) → C a p[a,b]
여기서 a는 데이터 타입 매개변수의 시퀀스이고, b는 생성자에 대한 인자의 시퀀스이며, p[a, b]는 이 구성이 귀속되는 귀납적 패밀리의 원소를 결정하는 인덱스입니다. (이 설명은 다소 오해의 소지가 있는데, 생성자에 대한 인자는 의존성이 성립하는 한 어떤 순서로도 나타날 수 있기 때문입니다.) C의 유니버스 레벨에 대한 제약은 귀납적 타입이 Prop(즉 Sort0)에 속하도록 지정되었는지 여부에 따라 두 가지 경우로 나뉩니다.
먼저 귀납적 타입이 Prop에 속하는 것으로 지정되지 않은 경우를 살펴봅시다. 그러면 유니버스 수준 u는 다음을 만족하도록 제약됩니다:
위와 같은 각 생성자 c에 대해, 그리고 시퀀스 β[a]의 각 βk[a]에 대해, βk[a] : Sort v이면, u ≥ v가 성립합니다.
다시 말해, 유니버스 레벨 u는 생성자의 인자를 나타내는 각 타입의 유니버스 레벨보다 크거나 같아야 합니다.
귀납적 타입이 Prop에 속하도록 지정된 경우, 생성자 인자의 유니버스 수준에는 아무런 제약이 없습니다. 하지만 이러한 유니버스 수준은 소거 규칙에 영향을 미칩니다. 일반적으로 Prop에 속하는 귀납적 타입의 경우, 소거 규칙의 모티브는 Prop에 속해야 합니다.
이 마지막 규칙에는 예외가 하나 있습니다. 생성자가 단 하나뿐이고 각 생성자 인자가 Prop에 속하거나 인덱스인 경우, 귀납적으로 정의된 Prop으로부터 임의의 Sort로 소거하는 것이 허용됩니다. 이러한 직관의 근거는, 이 경우 소거가 해당 인자의 타입이 거주자를 갖는다는 단순한 사실만으로 이미 주어진 것 이외의 어떠한 정보도 사용하지 않는다는 데 있습니다. 이 특수한 경우는 singleton elimination이라고 알려져 있습니다.
귀납적으로 정의된 동치 타입의 소거자인 Eq.rec의 적용에서 이미 단일 소거(singleton elimination)가 작동하는 것을 보았습니다. pa와 pb가 임의의 타입인 경우에도 원소 h:Eqab를 사용해 원소 h₂:pa를 pb로 형 변환할 수 있는데, 이는 이 변환이 새로운 데이터를 생성하지 않고 이미 가지고 있는 데이터를 재해석할 뿐이기 때문입니다. 단일 소거는 이종 동치(heterogeneous equality) 및 정초 재귀(well-founded recursion)와 함께도 사용되며, 이에 대해서는 귀납법과 재귀 장에서 다룰 것입니다.
이제 자주 유용하게 쓰이는 귀납적 타입의 두 가지 일반화를 살펴보겠습니다. Lean은 이를 앞서 설명한 더 원시적인 종류의 귀납적 타입으로 “컴파일”함으로써 지원합니다. 다시 말해, Lean은 더 일반화된 정의를 파싱하여 이를 바탕으로 보조 귀납적 타입을 정의하고, 그런 다음 이 보조 타입을 사용하여 우리가 실제로 원하는 타입을 정의합니다. 이러한 타입을 효과적으로 활용하려면 다음 장에서 설명할 Lean의 방정식 컴파일러가 필요합니다. 그럼에도 불구하고, 이러한 선언들은 일반적인 귀납적 정의의 단순한 변형에 지나지 않으므로 여기서 설명하는 것이 타당합니다.
첫째, Lean은 상호 정의된(mutually defined) 귀납적 타입을 지원합니다. 여기서 아이디어는 두 개(또는 그 이상)의 귀납적 타입을 동시에 정의할 수 있으며, 각각이 서로를 참조할 수 있다는 것입니다.
이 정의를 사용하면, α의 원소와 (비어 있을 수도 있는) 서브트리 리스트를 함께 제공하여 Treeα의 원소를 구성할 수 있습니다. 서브트리 리스트는 TreeListα 타입으로 표현되며, 이 타입은 빈 리스트인 nil이거나, 트리와 TreeListα의 원소로 이루어진 cons 중 하나로 정의됩니다.
하지만 이 정의는 다루기에 불편합니다. 서브트리 목록이 List(Treeα) 타입으로 주어진다면 훨씬 더 좋을 것입니다. 특히 Lean의 라이브러리에는 목록을 다루기 위한 수많은 함수와 정리가 포함되어 있기 때문입니다. TreeListα 타입이 List(Treeα)와 isomorphic함을 보일 수 있지만, 이 동형 사상을 따라 결과를 이리저리 변환하는 작업은 지루합니다.
이를 중첩된(nested) 귀납적 타입이라고 합니다. Tree가 mk의 인자들 사이에서 엄격하게 양의 위치에 나타나지 않고, 오히려 List 타입 생성자 내부에 중첩되어 있기 때문에, 이는 이전 절에서 제시한 귀납적 타입의 엄격한 명세를 벗어납니다. 그러면 Lean은 커널에서 TreeListα와 List(Treeα) 사이의 동형사상을 자동으로 구성하고, 이 동형사상을 바탕으로 Tree의 생성자들을 정의합니다.
곱셈, 전임자 함수(pred0=0을 만족하는), 절단 뺄셈(m이 n보다 크거나 같을 때 n-m=0을 만족하는), 거듭제곱 등 자연수에 대한 다른 연산들을 정의해 보십시오. 그런 다음 이미 증명한 정리들을 바탕으로 이러한 연산들의 기본적인 성질 중 일부를 증명해 보십시오.
이 중 다수는 이미 Lean의 코어 라이브러리에 정의되어 있으므로, 이름 충돌을 피하기 위해 Hidden이나 그와 비슷한 이름의 네임스페이스 안에서 작업해야 합니다.
length 함수나 reverse 함수와 같은 리스트 연산을 몇 가지 정의하십시오. 다음과 같은 성질들을 증명하십시오: