우리는 Lean의 형식적 기반에 기본 타입인 Prop, Type0, Type1, Type2, ...가 포함되어 있으며, 의존 함수 타입 (x:α)→β를 구성할 수 있다는 것을 살펴보았습니다. 예제에서는 Bool, Nat, Int와 같은 추가 타입과, List와 같은 타입 생성자, 그리고 곱 ×도 사용했습니다. 사실 Lean의 라이브러리에서 유니버스를 제외한 모든 구체적인 타입과 의존 화살표를 제외한 모든 타입 생성자는 귀납적 타입이라고 불리는 일반적인 타입 구성 계열의 인스턴스입니다. 타입 유니버스, 의존 화살표 타입, 귀납적 타입만을 기반으로 상당한 규모의 수학 체계를 구축할 수 있다는 것은 주목할 만합니다. 그 밖의 모든 것은 이것들로부터 따라 나옵니다.
직관적으로 말하면, 귀납적 타입은 지정된 생성자 목록으로부터 구성됩니다. Lean에서 이러한 타입을 지정하는 구문은 다음과 같습니다:
기본 발상은 각 생성자가 Foo의 새 객체를 만드는 방법을 하나씩 지정한다는 것이며, 이때 이미 만들어진 값을 재료로 쓸 수도 있습니다. Foo 타입은 이런 방식으로 만들어진 객체로만 이루어지며, 그 밖의 것은 포함하지 않습니다.
아래에서 살펴보겠지만, 생성자의 인자에는 Foo 타입의 객체가 포함될 수 있습니다. 다만 특정한 “양성(positivity)” 제약을 따라야 하며, 이 제약은 Foo의 원소가 아래에서부터 위로 구성됨을 보장합니다. 대략적으로 말하면, 각 ...는 Foo와 이전에 정의된 타입들로 구성된 어떤 화살표 타입이든 될 수 있습니다. 이때 Foo는 등장하더라도 의존 화살표 타입의 “대상(target)”으로만 등장합니다.
귀납적 타입의 예시를 여럿 제시하겠습니다. 또한 위 도식을 약간 일반화하여 상호 정의된 귀납적 타입과 이른바 귀납적 족도 살펴보겠습니다.
논리 연결사와 마찬가지로, 모든 귀납적 타입에는 그 타입의 원소를 구성하는 방법을 보여 주는 도입 규칙과, 그 타입의 원소를 다른 구성에서 “사용”하는 방법을 보여 주는 제거 규칙이 함께 주어집니다. 논리 연결사와 이렇게 닮았다는 것은 놀라운 일이 아닙니다. 아래에서 보겠지만, 논리 연결사 역시 귀납적 타입 구성의 예이기 때문입니다. 귀납적 타입의 도입 규칙은 이미 살펴보았습니다. 도입 규칙은 바로 해당 타입의 정의에 명시된 생성자들입니다. 제거 규칙은 그 타입에 대한 재귀의 원리를 제공하며, 여기에는 특수한 경우로서 귀납법의 원리도 포함됩니다.
다음 장에서는 Lean의 함수 정의 패키지를 설명합니다. 이 패키지를 사용하면 귀납적 타입 위에서 함수를 정의하고 귀납적 증명을 수행하기가 훨씬 더 편리해집니다. 하지만 귀납적 타입이라는 개념은 매우 근본적이므로, 저수준에서 직접 다루어 보며 이해하는 것부터 시작하는 것이 중요하다고 생각합니다. 먼저 귀납적 타입의 기본적인 예제 몇 가지로 시작하여, 점차 더 정교하고 복잡한 예제로 나아가겠습니다.
sunday, monday, ... , saturday를 Weekday의 서로 다른 원소로 생각하십시오. 이들 사이에는 그 밖에 구별되는 성질이 없습니다. 제거 원리 Weekday.rec은 타입 Weekday 및 그 생성자들과 함께 정의됩니다. 이것은 재귀자라고도 하며, 타입을 “귀납적”으로 만드는 것이 바로 이것입니다. 재귀자를 사용하면 각 생성자에 대응하는 값을 지정하여 Weekday 위의 함수를 정의할 수 있습니다. 직관적으로 말하면, 귀납적 타입은 생성자들에 의해 빠짐없이 생성되며 생성자들이 만들어 내는 것 외에는 다른 원소를 갖지 않습니다.
Lean의 논리를 사용할 때 match 표현식은 귀납적 타입을 선언할 때 생성되는 재귀자인 Weekday.rec를 사용하여 컴파일됩니다. 이를 통해 결과 항이 타입 이론 안에서 잘 정의됨이 보장됩니다. 컴파일된 코드에서는 match가 다른 함수형 프로그래밍 언어에서와 같은 방식으로 컴파일됩니다.
귀납적 데이터 타입을 선언할 때 derivingRepr를 사용하면 Weekday 객체를 텍스트로 변환하는 함수를 생성하도록 Lean에 지시할 수 있습니다. 이 함수는 #eval 명령이 Weekday 객체를 표시할 때 사용됩니다. Repr가 없으면 #eval은 그 자리에서 하나를 유도하려고 시도합니다.
어떤 구조체와 관련된 정의와 정리를 그 구조체와 같은 이름의 네임스페이스에 모아 두면 유용한 경우가 많습니다. 예를 들어, numberOfDay 함수를 Weekday 네임스페이스에 넣을 수 있습니다. 그러면 해당 네임스페이스를 열었을 때 더 짧은 이름을 사용할 수 있습니다.
아래의 귀납적 타입을 위한 택틱에서는 귀납적 타입을 활용하도록 특별히 설계된 추가 택틱들을 소개합니다.
명제-타입 대응 아래에서는 match를 사용하여 함수를 정의할 수 있을 뿐만 아니라 정리를 증명할 수도 있다는 점에 주목하십시오. 다시 말해, 명제-타입 대응 아래에서 경우에 따른 증명은 경우에 따른 정의의 일종이며, 여기서 “정의”되는 대상은 데이터 조각이 아니라 증명입니다.
(이 예제들을 실행하기 위해 예제들을 Hidden이라는 네임스페이스에 넣어, Bool 같은 이름이 표준 라이브러리의 Bool과 충돌하지 않게 합니다. 이러한 타입들은 시스템이 시작될 때 자동으로 임포트되는 Lean “프렐류드(prelude)”의 일부이기 때문에 이렇게 해야 합니다.)
연습 문제로, 이 타입들의 도입 규칙과 제거 규칙이 무엇을 하는지 생각해 보십시오. 추가 연습 문제로, Bool 타입에 불리언 연산 and, or, not을 정의하고 일반적인 항등식을 검증해 보기를 권합니다. and와 같은 이항 연산은 match를 사용하여 정의할 수 있다는 점에 유의하십시오:
이 예제들에서 무슨 일이 일어나고 있는지 생각해 보십시오. 곱 타입에는 두 개의 인자를 받는 생성자 Prod.mk 하나가 있습니다. Prodαβ 위의 함수를 정의할 때는 입력이 Prod.mkab 꼴이라고 가정할 수 있으며, 출력은 a와 b로 이루어진 항으로 지정해야 합니다. 이를 사용하여 Prod의 두 사영(projection)을 정의할 수 있습니다. 표준 라이브러리는 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는 구성하려는 객체의 타입을 지정하는 데 사용되며, 쌍에 의존할 수 있기 때문에 함수입니다. cond 함수는 불리언 조건문입니다. condbt1t2는 b가 참이면 t1을, 그렇지 않으면 t2를 반환합니다. 함수 prod_example은 불리언 b와 수 n으로 이루어진 쌍을 받아, b가 참인지 거짓인지에 따라 2*n 또는 2*n+1을 반환합니다.
반면에 합 타입에는 inl과 inr(각각 “왼쪽에 삽입(insert left)”과 “오른쪽에 삽입(insert right)”을 뜻합니다)이라는 두 개의 생성자가 있으며, 각 생성자는 하나의 (명시적) 인수를 받습니다. Sumαβ에 대한 함수를 정의하려면 두 가지 경우를 처리해야 합니다. 입력이 inla 형태라면 a를 사용하는 항으로 출력값을 지정해야 하고, 입력이 inrb 형태라면 b를 사용하는 항으로 출력값을 지정해야 합니다.
이 예제는 앞의 예제와 비슷하지만, 이번에는 sum_example의 입력이 암묵적으로 inln 또는 inrn 형태 중 하나입니다. 첫 번째 경우 함수는 2*n을 반환하고, 두 번째 경우에는 2*n+1을 반환합니다.
곱 타입이 매개변수 αβ : Type에 의존한다는 점에 주목하십시오. 이 매개변수들은 Prod뿐만 아니라 생성자의 인자이기도 합니다. Lean은 이러한 인자를 생성자의 뒤쪽 인자나 반환 타입으로부터 추론할 수 있는 경우를 감지하고, 그러한 경우에는 해당 인자를 암시적 인자로 만듭니다.
자연수 정의하기에서는 귀납적 타입의 생성자가 그 귀납적 타입 자체를 인자로 받을 때 어떤 일이 일어나는지 살펴보겠습니다. 이 절에서 다루는 예제들의 특징은 각 생성자가 앞서 지정된 타입에만 의존한다는 점입니다.
생성자가 여러 개인 타입은 선언적(disjunctive)이라는 점에 유의하십시오. Sumαβ의 원소는 inla 형태 또는inlb 형태 중 하나입니다. 인자가 여러 개인 생성자는 연언적(conjunctive) 정보를 도입합니다. Prodαβ의 원소 Prod.mkab로부터 a그리고b를 추출할 수 있습니다. 임의의 귀납적 타입은 생성자를 몇 개든 가질 수 있고 각 생성자가 인자를 몇 개든 받을 수 있으므로, 두 특징을 모두 포함할 수 있습니다.
함수 정의와 마찬가지로, Lean의 귀납적 정의 구문에서는 생성자에 대한 이름 있는 인자를 콜론 앞에 둘 수 있습니다:
Prod처럼 생성자가 하나뿐인 타입은 순수하게 연언적입니다. 생성자는 인자 목록을 하나의 데이터로 묶을 뿐이며, 이는 본질적으로 뒤따르는 인자의 타입이 첫 번째 인자의 타입에 의존할 수 있는 튜플입니다. 이러한 타입을 “레코드” 또는 “구조체”로 생각할 수도 있습니다. Lean에서는 structure 키워드를 사용하여 이러한 귀납적 타입과 그 사영(projection)을 동시에 정의할 수 있습니다.
의존 타입 이론의 의미론에는 부분 함수라는 개념이 기본적으로 내장되어 있지 않습니다. 함수 타입 α→β나 의존 함수 타입 (a:α)→β의 모든 원소는 모든 입력에 대해 값을 갖는다고 가정됩니다. Option 타입은 부분 함수를 표현하는 방법을 제공합니다. Optionβ의 원소는 none이거나, 어떤 값 b : β에 대해 someb 형태입니다. 따라서 타입 α→Optionβ의 원소 f를 α에서 β로 가는 부분 함수로 생각할 수 있습니다. 즉, 모든 a : α에 대해 fa는 fa가 “정의되지 않음”을 나타내는 none을 반환하거나, someb를 반환합니다.
Inhabitedα의 원소는 단순히 α의 원소가 존재한다는 사실에 대한 증거입니다. 나중에 Inhabited가 Lean에서 타입 클래스의 한 예라는 것을 보게 될 것입니다. Lean에게 적절한 기본 타입들이 원소를 가진다는 것을 알려 줄 수 있으며, Lean은 이를 바탕으로 다른 구성된 타입들도 원소를 가진다는 것을 자동으로 추론할 수 있습니다.
연습 문제로 α에서 β로 가는 부분 함수와 β에서 γ로 가는 부분 함수의 합성 개념을 정의하고, 이것이 예상대로 동작함을 보여 보시기를 권합니다. 또한 Bool과 Nat이 거주된(inhabited) 타입임을, 거주된 두 타입의 곱도 거주된 타입임을, 그리고 거주된 타입으로 가는 함수들의 타입도 거주된 타입임을 보여 보시기를 권합니다.
이것들이 어떻게 여러분이 이미 본 도입 규칙과 제거 규칙을 만들어 내는지 생각해 보십시오. 귀납적 타입의 제거자가 무엇으로 제거할 수 있는지, 즉 어떤 종류의 타입이 재귀자의 대상이 될 수 있는지를 규정하는 규칙들이 있습니다. 대략적으로 말하면, Prop에 속한 귀납적 타입의 특징은 Prop에 속한 다른 타입으로만 소거할 수 있다는 점입니다. 이는 p : Prop이면 원소 hp : p는 아무런 데이터도 담지 않는다는 이해와 일치합니다. 그러나 이 규칙에는 작은 예외가 하나 있으며, 이는 아래 귀납적 족에서 논의하겠습니다.
∃x:α,p 표기법은 Exists(funx:α=>p)의 문법적 설탕(syntactic sugar)이라는 점을 기억하십시오.
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 값이 이미 계산되었다고 가정할 수 있습니다. 그 결과, 재귀자의 다음 인자는 f(succn)의 값을 n과 fn의 항으로 지정합니다. 재귀자의 타입을 확인해 보면 다음과 같습니다.
암시적 인자 motive는 정의되는 함수의 공역입니다. 타입 이론에서는 motive를 제거/재귀의 동기(motive)라고 부르는 것이 일반적입니다. 이는 우리가 구성하고자 하는 대상의 종류를 기술하기 때문입니다. 다음 두 인수는 앞서 설명한 대로 0인 경우와 후속자인 경우를 계산하는 방법을 지정합니다. 이들은 소전제(minor premises)라고도 합니다. 마지막으로, t:Nat은 함수의 입력입니다. 이것은 대전제(major premise)라고도 합니다.
예를 들어, 자연수에 대한 덧셈 함수 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)을 증명합니다.
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이 된다는 점에 주목하십시오.
generalize에 의해 표현식 m+3*k가 지워진다는 점에 주목하십시오. 중요한 것은 그것이 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를 구성합니다. 여기서 “...”는 인자 타입들의 나열을 나타내며, 이를 인덱스라고도 부릅니다. 이때 각 생성자는 이 족(family)에 속한 어떤 구성원의 원소를 구성합니다. 한 예로 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를 적용할 때 싱글턴 제거가 작동하는 것을 이미 보았습니다. pa와 pb가 임의의 타입이더라도 원소 h:Eqab를 사용하여 원소 h₂:pa를 pb로 캐스트할 수 있습니다. 캐스트는 새로운 데이터를 만들어 내지 않고 이미 가진 데이터를 다시 해석할 뿐이기 때문입니다. 싱글턴 제거는 이종 상등과 정초 재귀에도 사용되며, 이에 대해서는 귀납법과 재귀에 관한 장에서 다룰 것입니다.
이제 자주 유용하게 쓰이는 귀납적 타입의 두 가지 일반화를 살펴보겠습니다. Lean은 이들을 앞에서 설명한 더 기본적인 종류의 귀납적 타입으로 “컴파일”하여 지원합니다. 다시 말해, Lean은 더 일반적인 정의를 파싱하고, 이를 바탕으로 보조 귀납적 타입을 정의한 다음, 그 보조 타입을 사용하여 우리가 실제로 원하는 타입을 정의합니다. 다음 장에서 설명할 Lean의 등식 컴파일러가 있어야 이 타입들을 효과적으로 활용할 수 있습니다. 그럼에도 이러한 선언은 일반적인 귀납적 정의를 간단히 변형한 것이므로 여기서 설명하는 것이 타당합니다.
먼저, Lean은 상호 정의된 귀납적 타입을 지원합니다. 두 개(또는 그 이상)의 귀납적 타입을 동시에 정의하면서 각 타입이 다른 타입을 참조하도록 할 수 있다는 것이 그 핵심입니다.
이 정의에 따르면, α의 원소 하나와 (비어 있을 수도 있는) 부분 트리의 리스트를 함께 제공하여 Treeα의 원소를 구성할 수 있습니다. 부분 트리의 리스트는 TreeListα 타입으로 표현되며, 이 타입은 빈 리스트인 nil이거나, 트리 하나와 TreeListα의 원소 하나에 cons를 적용한 것으로 정의됩니다.
하지만 이 정의는 다루기가 불편합니다. 하위 트리의 리스트가 List(Treeα) 타입으로 주어진다면 훨씬 좋을 것입니다. 특히 Lean의 라이브러리에는 리스트를 다루는 함수와 정리가 많이 들어 있기 때문입니다. TreeListα 타입이 List(Treeα)와 동형이라는 것을 보일 수는 있지만, 이 동형 사상을 따라 결과를 앞뒤로 옮기는 일은 번거롭습니다.
이를 중첩 귀납적 타입이라고 합니다. Tree가 mk의 인자들 사이에 엄격하게 양성적으로 나타나지 않고 오히려 List 타입 생성자 안에 중첩되어 있으므로, 이는 앞 절에서 제시한 귀납적 타입의 엄격한 명세를 벗어납니다. 그런 다음 Lean은 커널에서 TreeListα와 List(Treeα) 사이의 동형을 자동으로 구성하고, 이 동형을 사용한 항으로 Tree의 생성자들을 정의합니다.