효율적인 코드를 작성하려면 적절한 자료구조를 선택하는 것이 중요하다.
연결 리스트가 유용한 경우도 있다. 어떤 응용에서는 리스트의 꼬리를 공유하는 능력이 매우 중요하다.
그러나 길이가 변하는 순차 데이터 컬렉션의 대부분의 사용 사례에는 배열이 더 적합하다. 배열은 메모리 오버헤드가 더 작고 지역성도 더 좋다.
Nat.le을 정의하려면 아직 소개하지 않은 Lean의 기능이 필요하다. 이는 귀납적으로 정의된 관계다.
8.3.1.1. Inductively-Defined Propositions, Predicates, and Relations🔗
Nat.le은 귀납적으로 정의된 관계다.
inductive로 새 데이터 타입을 만들 수 있듯이 새 명제도 만들 수 있다.
명제가 인자를 받으면 가능한 인자 중 일부에 대해서는 참일 수 있지만 모두에 대해서는 그렇지 않을 수 있는 술어라고 한다.
여러 인자를 받는 명제를 관계라고 한다.
귀납적으로 정의된 명제의 각 생성자는 그 명제를 증명하는 방법이다.
다시 말해 명제의 선언은 명제가 참이라는 여러 형태의 증거를 기술한다.
인자가 없고 생성자가 하나인 명제는 매우 쉽게 증명할 수 있다.
실제로 항상 쉽게 증명할 수 있어야 하는 명제 True도 EasyToProve와 같은 방식으로 정의되어 있다.
inductiveTrue:Propwhere|intro:True
인자를 받지 않는 귀납적으로 정의된 명제는 귀납적으로 정의된 데이터 타입만큼 흥미롭지 않다.
데이터는 그 자체로 흥미롭기 때문이다. 자연수 3은 35와 다르며, 피자 3개를 주문한 사람의 집에 30분 뒤 35개가 도착하면 당황할 것이다.
명제의 생성자는 명제가 참일 수 있는 방법을 기술하지만, 명제를 증명하고 나면 어떤 생성자를 사용했는지는 알 필요가 없다.
그래서 Prop 유니버스의 흥미로운 귀납 타입 대부분은 인자를 받는다.
표준 거짓 명제 False에는 생성자가 없으므로 직접 증거를 제공할 수 없다.
False의 증거를 제공하는 유일한 방법은 가정 자체가 불가능한 경우다. 이는 타입 시스템이 도달 불가능하다고 판단하는 코드를 nomatch로 표시하는 방식과 비슷하다.
증명에 관한 첫 막간에서 설명했듯이 부정 NotA는 A→False의 줄임말이다.
NotA는 ¬A로도 쓸 수 있다.
매개변수 n은 더 작아야 하는 수이고, 인덱스는 n 이상이어야 하는 수다.
두 수가 같을 때는 refl 생성자를 사용하고, 인덱스가 n보다 클 때는 step 생성자를 사용한다.
증명의 관점에서 n \leq k의 증명은 n + d = k가 되도록 어떤 수 d를 찾는 것이다.
Lean에서 증명은 Nat.le.refl 생성자를 d개의 Nat.le.step으로 감싼 형태다.
각 step 생성자는 인덱스 인자에 1을 더하므로 d개의 step 생성자는 큰 수에 d를 더한다.
예를 들어 4가 7 이하라는 증거는 refl을 세 개의 step으로 감싼 것이다.
Array.map 함수는 함수로 배열을 변환하고, 입력 배열의 각 원소에 함수를 적용한 결과를 담은 새 배열을 반환한다.
이를 꼬리 재귀 함수로 작성할 때는 출력 배열을 누산기로 전달하는 함수에 위임하는 일반적인 패턴을 따른다.
누산기는 빈 배열로 초기화한다.
누산기를 전달하는 도우미 함수는 배열의 현재 인덱스를 추적하는 인자도 받으며, 이 인덱스는 0에서 시작한다.
도우미 함수는 매 반복에서 인덱스가 여전히 범위 안에 있는지 검사해야 한다.
범위 안에 있으면 변환한 원소를 누산기 끝에 추가하고 인덱스를 1 증가시켜 다시 반복해야 한다.
그렇지 않으면 종료하고 누산기를 반환해야 한다.
이 코드의 초기 구현은 배열 인덱스가 유효함을 Lean이 증명하지 못하므로 실패한다.
defarrayMapHelper(f:α→β)(arr:Arrayα)(soFar:Arrayβ)(i:Nat):Arrayβ:=ifi<arr.sizethenarrayMapHelperfarr(soFar.push(ffailed to prove index is valid, possible solutions: - Use `have`-expressions to prove the index is valid - Use `a[i]!` notation instead, runtime check is performed, and 'Panic' error message is produced if index is not valid - Use `a[i]?` notation instead, result is an `Option` type - Use `a[i]'h` notation instead, where `h` is a proof that index is validα:Type ?u.7β:Type ?u.9f:α→βarr:ArrayαsoFar:Arrayβi:Nat⊢ i<arr.sizearr[i]))(i+1)elsesoFar
failed to prove index is valid, possible solutions: - Use `have`-expressions to prove the index is valid - Use `a[i]!` notation instead, runtime check is performed, and 'Panic' error message is produced if index is not valid - Use `a[i]?` notation instead, result is an `Option` type - Use `a[i]'h` notation instead, where `h` is a proof that index is validα:Type ?u.7β:Type ?u.9f:α→βarr:ArrayαsoFar:Arrayβi:Nat⊢ i<arr.size
그러나 조건 표현식은 배열 인덱스의 유효성에 필요한 정확한 조건, 즉 i<arr.size를 이미 검사한다.
if에 이름을 붙이면 배열 인덱싱 전술이 사용할 수 있는 가정이 추가되므로 문제가 해결된다.
재귀 호출이 입력 생성자의 인자 중 하나에 대해 이루어지지 않는데도 Lean은 수정된 프로그램을 받아들인다.
실제로 누산기와 인덱스는 줄어들지 않고 모두 커진다.
내부적으로 Lean의 증명 자동화가 종료 증명을 구성한다.
이 증명을 다시 구성해 보면 Lean이 자동으로 인식하지 못하는 경우를 더 쉽게 이해할 수 있다.
arrayMapHelper는 왜 종료하는가?
매 반복에서 인덱스 i가 배열 arr의 범위 안에 있는지 검사한다.
범위 안에 있으면 i를 증가시키고 루프를 반복한다.
그렇지 않으면 프로그램이 종료한다.
arr.size는 유한한 수이므로 i도 유한한 횟수만 증가할 수 있다.
호출마다 함수의 인자가 감소하지 않더라도 arr.size-i는 0을 향해 감소한다.
각 재귀 호출에서 감소하는 값을 측정값(measure)이라고 한다.
정의 끝에 termination_by 절을 제공하면 Lean에게 특정 표현식을 종료 측정값으로 사용하라고 지시할 수 있다.
arrayMapHelper의 명시적 측정값은 다음과 같다.
모든 종료 논증이 이만큼 간단한 것은 아니다.
그러나 함수의 인자를 바탕으로 호출마다 감소할 표현식을 식별한다는 기본 구조는 모든 종료 증명에 나타난다.
때로는 함수가 왜 종료하는지 알아내기 위해 창의성이 필요하고, 때로는 측정값이 실제로 감소한다는 것을 받아들이도록 Lean에 추가 증명을 제공해야 한다.