α가 임의의 타입이라면, α 위의 단항 술어 p를 α→Prop 타입의 객체로 표현할 수 있다는 점에 주목하십시오. 이 경우 x : α가 주어지면, px는 p가 x에 대해 성립한다는 주장을 나타냅니다. 마찬가지로, 객체 r : α→α→Prop는 α 위의 이항 관계를 나타냅니다. xy : α가 주어지면, rxy는 x가 y와 관계되어 있다는 주장을 나타냅니다.
전칭 한정자 ∀x:α,px는 “모든 x : α에 대해 px가 성립한다”는 주장을 나타내도록 되어 있습니다. 명제 연결사와 마찬가지로, 자연 연역 체계에서 “forall”은 도입 규칙과 제거 규칙의 지배를 받습니다. 비형식적으로 말하면, 도입 규칙은 다음과 같습니다:
x : α가 임의인 맥락에서 px의 증명이 주어지면, 증명 ∀x:α,px를 얻습니다.
제거 규칙은 다음과 같습니다:
∀x:α,px의 증명과 임의의 항 t : α가 주어지면 pt의 증명을 얻습니다.
함의의 경우와 마찬가지로, 이제 명제-타입 대응(propositions-as-types) 해석이 작용합니다. 의존 화살표 타입의 도입 규칙과 제거 규칙을 떠올려 보십시오:
x : α가 임의인 문맥에서 βx 타입의 항 t가 주어지면, (funx:α=>t):(x:α)→βx를 얻습니다.
제거 규칙은 다음과 같습니다.
항 s : (x:α)→βx와 임의의 항 t : α가 주어지면 st:βt입니다.
px의 타입이 Prop인 경우, (x:α)→βx를 ∀x:α,px로 바꾸면 이 규칙들을 전칭 한정자가 포함된 증명을 구성하는 올바른 규칙으로 읽을 수 있습니다.
따라서 구성 계산법(Calculus of Constructions)은 이러한 방식으로 의존 화살표 타입을 forall 표현식과 동일시합니다. p가 임의의 표현식이라면, ∀x:α,p는 (x:α)→p의 대체 표기법에 지나지 않습니다. 이는 p가 명제인 경우에는 전자가 후자보다 더 자연스럽다는 생각에서 비롯된 것입니다. 일반적으로 표현식 p는 x:α에 의존합니다. 일반적인 함수 공간의 경우, α→β를 β가 x에 의존하지 않는 (x:α)→β의 특수한 경우로 해석할 수 있었다는 점을 떠올려 보십시오. 마찬가지로, 명제 사이의 함의 p→q도 표현식 q가 x에 의존하지 않는 ∀x:p,q의 특수한 경우로 생각할 수 있습니다.
표기법상의 관례로, 전칭 한정자에는 가능한 한 가장 넓은 범위를 부여합니다. 따라서 위 예제에서 x에 대한 한정자의 범위를 가설로 제한하려면 괄호가 필요합니다. ∀y:α,py를 증명하는 표준적인 방법은 임의의 y를 취하여 py를 증명하는 것입니다. 이것이 도입 규칙입니다. 이제 h가 ∀x:α,px∧qx 타입을 가진다고 하면, 표현식 hy는 py ∧ qy 타입을 가집니다. 이것이 제거 규칙입니다. 왼쪽 연언지를 취하면 원하는 결론인 py를 얻습니다.
속박 변수의 이름을 바꾼 것만 다른 표현식들은 동치로 간주된다는 점을 기억하십시오. 따라서 예를 들어, 가설과 결론 모두에 같은 변수 x를 사용하고, 증명에서는 이를 다른 변수 z로 인스턴스화할 수도 있었습니다:
여기서 어떤 일이 일어나고 있는지 생각해 보십시오. trans_r을 값 abc로 인스턴스화하면, 결국 rab → rbc → rac의 증명을 얻게 됩니다. 이것을 “가설” hab:rab에 적용하면, 함의 rbc → rac의 증명을 얻습니다. 마지막으로, 이것을 가설 hbc에 적용하면 결론 rac의 증명을 얻습니다.
이와 같은 상황에서는 인수 abc를 habhbc로부터 추론할 수 있는데도 일일이 제공해야 하므로 번거로울 수 있습니다. 그러한 이유로 이 인수들을 암시적으로 만드는 것이 일반적입니다:
이 방식의 장점은 rac의 증명으로 trans_rhabhbc라고 간단히 쓸 수 있다는 것입니다. 단점은 trans_r와 trans_rhab 표현식에서 인자의 타입을 추론하는 데 필요한 정보가 Lean에 충분하지 않다는 것입니다. 첫 번째 #check 명령의 출력은 r ?m.1 ?m.2 → r ?m.2 ?m.3 → r ?m.1 ?m.3이며, 이는 이 경우 암시적 인자가 지정되지 않았음을 나타냅니다.
Prop을 다른 타입과 구별 짓는 것은 바로 의존 화살표 타입, 특히 전칭 한정자에 대한 타이핑 규칙입니다. α : Sorti와 β : Sortj가 있다고 가정합시다. 여기서 표현식 β는 변수 x : α에 의존할 수 있습니다. 그러면 (x:α)→β는 Sort(imaxij)의 원소입니다. 여기서 imax i j는 j가 0이 아니면 i와 j 중 최댓값이고, 그렇지 않으면 0입니다.
개념은 다음과 같습니다. j가 0이 아니면 (x:α)→β는 Sort(maxij)의 원소입니다. 다시 말해, α에서 β로 가는 의존 함수의 타입은 i와 j 중 최댓값을 인덱스로 갖는 유니버스에 “속합니다”. 그런데 β가 Sort0에 속한다고, 즉 Prop의 원소라고 가정해 봅시다. 이 경우 α가 어느 타입 유니버스에 속하든 관계없이 (x:α)→β 역시 Sort0의 원소입니다. 다시 말해, β가 α에 의존하는 명제라면 ∀x:α,β 역시 명제입니다. 이는 Prop을 데이터가 아닌 명제의 타입으로 보는 해석을 반영하며, Prop을 비서술적(impredicative)으로 만드는 것이 바로 이 성질입니다.
“서술적(predicative)”이라는 용어는 20세기로 넘어갈 무렵의 기초론적 발전에서 유래합니다. 당시 푸앵카레와 러셀 같은 논리학자들은 집합론의 역설이 “악순환”에서 비롯된다고 보았습니다. 이 악순환은 정의하려는 바로 그 성질을 항목으로 포함하는 모임에 대해 한정하여 그 성질을 정의할 때 생깁니다. α가 임의의 타입이면 α 위의 모든 술어로 이루어진 타입 α→Prop(“α의 멱타입”)을 만들 수 있다는 점에 주목하십시오. Prop이 비술어적(impredicative)이라는 것은 α→Prop에 대해 양화하는 명제를 만들 수 있다는 뜻입니다. 특히 α 위의 모든 술어에 대해 양화하여 α 위의 술어를 정의할 수 있습니다. 이것이 바로 한때 문제가 있다고 여겨졌던 종류의 순환성입니다.
반사성(reflexivity)은 보기보다 강력합니다. 구성 계산법(Calculus of Constructions)의 항은 계산적 해석을 가지며, 논리 체계는 공통된 축약형(reduct)을 갖는 항들을 같은 것으로 취급한다는 점을 상기하십시오. 그 결과, 자명하지 않은 몇몇 항등식도 반사성으로 증명할 수 있습니다:
그러나 상등은 단순한 동치 관계보다 훨씬 더 많은 것을 담고 있습니다. 상등은 모든 주장이 이 동치를 존중한다는 중요한 성질을 가지는데, 이는 같은 표현식끼리는 진릿값을 바꾸지 않고 서로 치환할 수 있다는 의미입니다. 다시 말해, h1 : a=b와 h2 : pa가 주어지면 치환을 이용하여 pb에 대한 증명을 구성할 수 있습니다: Eq.substh1h2.
두 번째 표기에 쓰인 삼각형은 Eq.subst와 Eq.symm을 바탕으로 만든 매크로이며, \t를 입력하여 쓸 수 있습니다.
Eq.subst 규칙은 더 명시적인 치환을 수행하는 다음 보조 규칙들을 정의하는 데 사용됩니다. 이 규칙들은 적용 형태의 항, 즉 st 형태의 항을 다루도록 설계되었습니다. 구체적으로, congrArg는 인자를 치환하는 데, congrFun은 적용되는 항을 치환하는 데, congr는 둘 다를 한꺼번에 치환하는 데 사용할 수 있습니다.
Eq.subst의 두 번째 암시적 매개변수는 치환이 일어날 문맥을 제공하며, 그 타입이 α→Prop이라는 점에 주목하십시오. 따라서 이 술어를 추론하려면 고차 단일화(higher-order unification)의 한 사례를 풀어야 합니다. 가장 일반적인 경우에 고차 단일자가 존재하는지 판정하는 문제는 결정 불가능하며, Lean은 기껏해야 이 문제에 대해 불완전하고 근사적인 해만을 제공할 수 있습니다. 그 결과, Eq.subst가 항상 원하는 대로 동작하지는 않습니다. 매크로 h▸e는 이 암시적 매개변수를 계산하는 데 더 효과적인 휴리스틱을 사용하며, Eq.subst를 적용하면 실패하는 상황에서도 성공하는 경우가 많습니다.
등식 추론은 매우 흔하고 중요하기 때문에, Lean은 이를 더 효과적으로 수행할 수 있는 여러 메커니즘을 제공합니다. 다음 절에서는 계산적 증명을 더 자연스럽고 명료하게 작성할 수 있게 해 주는 구문을 소개합니다. 하지만 더 중요한 것은, 등식 추론이 항 재작성기(term rewriter), 단순화기(simplifier), 그리고 그 밖의 여러 자동화 도구에 의해 지원된다는 점입니다. 항 재작성기와 단순화기는 다음 절에서 간략히 설명하고, 이어서 다음 장에서 더 자세히 설명합니다.
반면 simp 택틱은 주어진 항등식들을 항 안에서 적용할 수 있는 곳이면 어디에서든 순서에 상관없이 반복적으로 적용하여 목표를 다시 씁니다. 또한 이 택틱은 시스템에 미리 선언되어 있던 다른 규칙들도 사용하며, 무한 루프에 빠지지 않도록 교환법칙을 적절히 적용합니다. 그 결과, 이 정리를 다음과 같이 증명할 수도 있습니다:
위 예제는 관계에 대한 중위 표기법이 없더라도 calc를 사용할 수 있다는 점도 분명히 보여 줍니다. Lean에는 이미 나누어떨어짐을 나타내는 표준 유니코드 표기법(\dvd 또는 \mid로 입력할 수 있는 ∣를 사용합니다)이 포함되어 있으므로, 위 예제에서는 충돌을 피하기 위해 일반 세로 막대를 사용합니다. 실제로는 match ... with 표현식에서 사용되는 ASCII |와 혼동될 위험이 있으므로 좋은 방법이 아닙니다.
여기서 Nat.add_assoc 앞의 왼쪽 화살표는 rewrite가 해당 항등식을 반대 방향으로 사용하도록 지시합니다. (이 화살표는 \l로 입력하거나, 이에 해당하는 ASCII 표기인 <-를 사용할 수 있습니다.) 간결함이 목표라면 rw와 simp 모두 단독으로 이 작업을 해낼 수 있습니다:
Exists.intro에는 암시적 인자가 있다는 점에 주목하십시오. Lean은 결론 ∃x,px에 있는 술어 p : α→Prop를 추론해야 합니다. 이는 결코 간단한 일이 아닙니다. 예를 들어 hg : g00=0이 있을 때 Exists.intro0hg라고 쓰면, 술어 p에 가능한 값은 여러 가지이며, 각각 정리 ∃x,gxx=x, ∃x,gxx=0, ∃x,gx0=x 등에 대응합니다. Lean은 문맥을 사용하여 이 중 어느 것이 적절한지 추론합니다. 이는 다음 예제에서 확인할 수 있습니다. 이 예제에서는 Lean의 pretty-printer가 암시적 인자를 표시하도록 옵션 pp.explicit를 true로 설정합니다.
Exists.intro는 주장의 본문에 대한 증거를 숨기므로, 정보를 숨기는 연산으로 볼 수 있습니다. 존재 제거 규칙인 Exists.elim은 그 반대 연산을 수행합니다. 이 규칙을 사용하면 임의의 값 w에 대해 pw로부터 q가 따라 나옴을 보임으로써 ∃x:α,px로부터 명제 q를 증명할 수 있습니다. 대략적으로 말하면, px를 만족하는 x가 존재한다는 것을 알고 있으므로 그것에 이름을 붙일 수 있습니다. 예를 들어 w라고 부릅시다. q가 w를 언급하지 않는다면, pw로부터 q가 따라 나옴을 보이는 것은 그러한 x가 존재한다는 사실만으로 q가 따라 나옴을 보이는 것과 마찬가지입니다. 다음은 예시입니다
존재 제거 규칙을 논리합 제거 규칙과 비교해 보면 도움이 될 수 있습니다. ∃x:α,px라는 주장은 a가 α의 모든 원소를 순회할 때의 명제 pa들을 모두 이은 커다란 논리합으로 생각할 수 있습니다. 익명 생성자 표기법 ⟨w,hw.right,hw.left⟩는 중첩된 생성자 적용을 축약한 것임에 유의하십시오. 이를 ⟨w, ⟨hw.right, hw.left⟩⟩와 같이 써도 똑같습니다.
존재 명제는 의존 타입 절에서 설명한 시그마 타입과 매우 비슷하다는 점에 주목하십시오. 차이점은 존재 명제는 명제이고, 시그마 타입은 타입이라는 것입니다. 그 밖의 점에서는 둘이 매우 비슷합니다. 술어 p : α→Prop과 타입족 β : α→Type이 주어졌을 때, h : pa와 h':βa를 만족하는 항 a : α에 대해 항 Exists.introah는 타입 (∃x:α,px):Prop을 가지고, Sigma.mkah'는 타입 (Σx:α,βx)를 가집니다. ∃와 Σ 사이의 유사성은 커리-하워드 동형의 또 다른 사례입니다.
Lean은 match 표현식을 사용하여 존재 한정자를 제거하는 더 편리한 방법을 제공합니다:
match 표현식은 Lean의 함수 정의 시스템의 일부이며, 이 시스템은 복잡한 함수를 편리하고 표현력 있게 정의하는 방법을 제공합니다. 이번에도 커리-하워드 동형 덕분에 이 메커니즘을 증명 작성에도 활용할 수 있습니다. match 문은 존재 주장을 구성 요소 w와 hw로 "분해"하며, 이 구성 요소들은 문의 본문에서 명제를 증명하는 데 사용될 수 있습니다. 더 명확하게 하기 위해 match에서 사용되는 타입을 명시할 수 있습니다:
구성적 “또는”이 고전적 “또는”보다 강한 것과 마찬가지로, 구성적 “존재한다” 역시 고전적 “존재한다”보다 강합니다. 예를 들어, 다음 함의에는 고전적 추론이 필요합니다. 구성적 관점에서 보면, 모든 x가 ¬p를 만족하는 것은 아니라는 사실을 아는 것은 p를 만족하는 특정한 x를 가지고 있는 것과 같지 않기 때문입니다.
두 번째 예제와 마지막 두 예제에는 α 타입의 원소 a가 적어도 하나 존재한다는 가정이 필요하다는 점에 유의하십시오.
다음은 그중 더 어려운 문제 두 개의 풀이입니다:
openClassicalvariable(α:Type)(pq:α→Prop)variable(a:α)variable(r:Prop)example:(∃x,px∨qx)↔(∃x,px)∨(∃x,qx):=Iff.intro(fun⟨a,(h1:pa∨qa)⟩=>Or.elimh1(funhpa:pa=>Or.inl⟨a,hpa⟩)(funhqa:qa=>Or.inr⟨a,hqa⟩))(funh:(∃x,px)∨(∃x,qx)=>Or.elimh(fun⟨a,hpa⟩=>⟨a,(Or.inlhpa)⟩)(fun⟨a,hqa⟩=>⟨a,(Or.inrhqa)⟩))example:(∃x,px→r)↔(∀x,px)→r:=Iff.intro(fun⟨b,(hb:pb→r)⟩=>funh2:∀x,px=>showrfromhb(h2b))(funh1:(∀x,px)→r=>show∃x,px→rfrombyCases(funhap:∀x,px=>⟨a,λVariable name `h'` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_h'Note: This linter can be disabled with `set_option linter.unusedVariables false`h'=>h1hap⟩)(funhnap:¬∀x,px=>byContradiction(funhnex:¬∃x,px→r=>havehap:∀x,px:=funx=>byContradiction(funhnp:¬px=>havehex:∃x,px→r:=⟨x,(funhp=>absurdhphnp)⟩showFalsefromhnexhex)showFalsefromhnaphap)))
이는 Lean에게 assumption 택틱을 사용하라고 지시하며, 이 택틱은 다시 로컬 컨텍스트에서 적절한 가설을 찾아 목표를 증명합니다. assumption 택틱에 대해서는 다음 장에서 더 자세히 배우겠습니다.
또한 ‹p›라고 작성하여 Lean에게 증명을 채워 달라고 요청할 수도 있습니다. 여기서 p는 Lean이 맥락에서 증명을 찾아 주기를 바라는 명제입니다. 이 꺾쇠 따옴표는 각각 \f<와 \f>로 입력할 수 있습니다. 문자 “f”는 “French(프랑스어)”를 뜻하는데, 이 유니코드 기호들이 프랑스어 인용 부호로도 쓰일 수 있기 때문입니다. 실제로 이 표기법은 Lean에서 다음과 같이 정의되어 있습니다:
매개변수가 없다면 Prop 타입의 표현식은 단지 하나의 주장일 뿐이라는 점을 기억하십시오. 아래에 있는 prime과 Fermat_prime의 정의를 채워 넣고, 주어진 각 주장을 구성하십시오. 예를 들어, 모든 자연수 n에 대해 n보다 큰 소수가 존재한다고 주장함으로써 소수가 무한히 많다는 것을 표현할 수 있습니다. 골드바흐의 약한 추측은 5보다 큰 모든 홀수가 세 소수의 합이라고 주장합니다. 필요하다면 페르마 소수의 정의나 다른 명제들을 찾아보십시오.