1.6. Polymorphism
대부분의 언어와 마찬가지로 Lean의 타입도 인수를 받을 수 있다.
예를 들어 List Nat 타입은 자연수 목록을, List String은 문자열 목록을, List (List Point)는 점 목록의 목록을 나타낸다.
이는 C#이나 Java 같은 언어의 List<Nat>, List<String>, List<List<Point>>와 매우 비슷하다.
Lean이 함수에 인수를 전달할 때 공백을 사용하듯이 타입에 인수를 전달할 때도 공백을 사용한다.
함수형 프로그래밍에서 다형적(polymorphic)이라는 용어는 보통 타입을 인수로 받는 데이터 타입과 정의를 가리킨다. 이는 객체 지향 프로그래밍에서 상위 클래스의 동작을 일부 재정의할 수 있는 하위 클래스를 가리키는 것과 다르다. 이 책에서 “다형성”은 항상 첫 번째 의미를 가리킨다. 이러한 타입 인수는 데이터 타입이나 정의에서 사용할 수 있으므로, 인수 이름을 다른 타입으로 바꾸어 얻은 어떤 타입에도 같은 데이터 타입이나 정의를 사용할 수 있다.
Point 구조체는 x와 y 필드가 모두 Float이어야 한다.
하지만 점의 각 좌표를 특정 방식으로 표현해야 할 이유는 없다.
Point의 다형적 버전인 PPoint는 타입을 인수로 받아 두 필드에 모두 사용할 수 있다.
structure PPoint (α : Type) where
x : α
y : α
함수 정의의 인수를 정의하는 이름 바로 뒤에 쓰듯이 구조체의 인수도 구조체 이름 바로 뒤에 쓴다.
더 구체적인 이름이 떠오르지 않을 때 Lean에서는 타입 인수에 그리스 문자를 이름으로 사용하는 것이 관례다.
Type은 다른 타입을 설명하는 타입이므로 Nat, List String, PPoint Int는 모두 Type 타입이다.
List와 마찬가지로 PPoint도 특정 타입을 인수로 제공해 사용할 수 있다:
def natOrigin : PPoint Nat :=
{ x := Nat.zero, y := Nat.zero }
이 예에서는 두 필드 모두 Nat이어야 한다.
함수 호출이 인수 변수를 인수 값으로 바꾸어 이루어지듯이, PPoint에 Nat 타입을 인수로 제공하면 x와 y 필드가 Nat 타입인 구조체를 얻는다. 인수 이름 α가 인수 타입 Nat으로 바뀌었기 때문이다.
Lean에서 타입은 일반적인 표현식이므로 PPoint와 같은 다형적 타입에 인수를 전달할 때 특별한 문법이 필요하지 않다.
정의도 타입을 인수로 받을 수 있으며, 그러면 다형적이 된다.
replaceX 함수는 PPoint의 x 필드를 새 값으로 바꾼다.
replaceX가 어떤 다형적 점에도 작동하게 하려면 이 함수 자체도 다형적이어야 한다.
첫 번째 인수를 점 필드의 타입으로 만들고 뒤의 인수가 첫 번째 인수의 이름을 다시 참조하게 하면 된다.
def replaceX (α : Type) (point : PPoint α) (newX : α) : PPoint α :=
{ point with x := newX }
다시 말해 point와 newX 인수의 타입에서 α를 언급하면 첫 번째 인수로 제공된 어떤 타입이든 참조한다.
이는 함수 본문에서 함수 인수 이름이 제공된 값을 참조하는 방식과 비슷하다.
Lean에 replaceX의 타입을 확인하게 한 다음 replaceX Nat의 타입을 확인하게 하면 이를 볼 수 있다.
#check (replaceX)
이 함수 타입에는 첫 번째 인수의 이름이 포함되어 있으며 타입의 뒤 인수는 이 이름을 다시 참조한다.
함수 적용의 값이 함수 본문에서 인수 이름을 제공된 인수 값으로 바꾸어 구해지듯이, 함수 적용의 타입은 함수 반환 타입에서 인수 이름을 제공된 값으로 바꾸어 구한다.
첫 번째 인수 Nat을 제공하면 타입의 나머지 부분에 있는 α의 모든 출현이 Nat으로 바뀐다:
#check replaceX Nat나머지 인수에는 명시적인 이름이 없으므로 인수를 더 제공해도 추가 치환은 일어나지 않는다:
#check replaceX Nat natOrigin#check replaceX Nat natOrigin 5다형적 함수는 이름 있는 타입 인수를 받고 뒤의 타입이 그 인수의 이름을 참조하는 방식으로 작동한다. 하지만 타입 인수만 이름을 붙일 수 있는 특별한 이유가 있는 것은 아니다. 양수 또는 음수 부호를 나타내는 데이터 타입이 주어졌다고 하자:
inductive Sign where
| pos
| neg
부호를 인수로 받는 함수를 작성할 수 있다.
인수가 양수이면 함수는 Nat을 반환하고 음수이면 Int를 반환한다:
def posOrNegThree (s : Sign) :
match s with | Sign.pos => Nat | Sign.neg => Int :=
match s with
| Sign.pos => (3 : Nat)
| Sign.neg => (-3 : Int)
타입은 일급이고 Lean 언어의 일반 규칙으로 계산할 수 있으므로 데이터 타입에 패턴 매칭해 계산할 수도 있다.
Lean은 이 함수를 확인할 때 함수 본문의 match 식이 타입의 match 식에 대응한다는 사실을 이용해 pos 경우의 예상 타입을 Nat으로, neg 경우의 예상 타입을 Int로 만든다.
posOrNegThree을 pos에 적용하면 함수 본문과 반환 타입 모두에서 인수 이름 s가 pos로 바뀐다.
표현식과 그 타입 모두에서 평가가 일어날 수 있다:
1.6.1. Linked Lists
Lean 표준 라이브러리에는 List라는 표준 연결 리스트 데이터 타입과 이를 편리하게 사용하는 특별한 문법이 있다.
목록은 대괄호로 쓴다.
예를 들어 10보다 작은 소수를 포함하는 목록은 다음과 같이 쓸 수 있다:
def primesUnder10 : List Nat := [2, 3, 5, 7]
이면에서 List는 다음과 같이 정의된 귀납적 데이터 타입이다:
inductive List (α : Type) where
| nil : List α
| cons : α → List α → List α
표준 라이브러리의 실제 정의는 아직 소개하지 않은 기능을 사용하므로 조금 다르지만 본질적으로 비슷하다.
이 정의는 List가 PPoint와 마찬가지로 하나의 타입을 인수로 받는다는 뜻이다.
이 타입은 목록에 저장되는 원소의 타입이다.
생성자에 따르면 List α는 nil이나 cons로 만들 수 있다.
nil 생성자는 빈 목록을 나타내고 cons 생성자는 비어 있지 않은 목록에 사용한다.
cons의 첫 번째 인수는 목록의 머리이고 두 번째 인수는 꼬리다.
n개의 원소를 가진 목록에는 n개의 cons 생성자가 있으며, 마지막 생성자의 꼬리는 nil이다.
List의 생성자를 직접 사용하면 primesUnder10 예제를 더 명시적으로 쓸 수 있다:
def explicitPrimesUnder10 : List Nat :=
List.cons 2 (List.cons 3 (List.cons 5 (List.cons 7 List.nil)))
이 두 정의는 완전히 동등하지만 primesUnder10이 explicitPrimesUnder10보다 훨씬 읽기 쉽다.
List를 소비하는 함수는 Nat을 소비하는 함수와 거의 같은 방식으로 정의할 수 있다.
연결 리스트를 각 succ 생성자에 추가 데이터 필드가 매달린 Nat으로 생각할 수도 있다.
이 관점에서 목록의 길이를 계산하는 것은 각 cons를 succ로, 마지막 nil을 zero로 바꾸는 과정이다.
replaceX가 점 필드의 타입을 인수로 받았듯이 length는 목록 원소의 타입을 받는다.
예를 들어 목록에 문자열이 들어 있으면 첫 번째 인수는 String이다: length String ["Sourdough", "bread"].
다음과 같이 계산된다:
length의 정의는 리스트 원소 타입을 인수로 받으므로 다형적이고, 자기 자신을 참조하므로 재귀적이다.
일반적으로 함수는 데이터의 모양을 따른다. 재귀적 데이터 타입은 재귀 함수로, 다형적 데이터 타입은 다형적 함수로 이어진다.
def length (α : Type) (xs : List α) : Nat :=
match xs with
| List.nil => Nat.zero
| List.cons y ys => Nat.succ (length α ys)
xs와 ys 같은 이름은 관례적으로 알 수 없는 값의 목록을 나타내는 데 사용한다.
이름의 s는 복수형을 나타내므로 “x s”, “y s”가 아니라 “엑시스”, “와이스”라고 발음한다.
1.6.2. Implicit Arguments
replaceX와 length는 뒤의 값으로 타입 인수가 보통 유일하게 결정되므로 사용하기에 다소 번거롭다.
실제로 대부분의 언어에서 컴파일러는 타입 인수를 스스로 잘 결정하며 사용자의 도움은 가끔만 필요하다.
Lean도 마찬가지다.
함수를 정의할 때 인수를 괄호 대신 중괄호로 감싸 암시적이라고 선언할 수 있다.
예를 들어 암시적 타입 인수를 사용하는 replaceX 버전은 다음과 같다:
def replaceX {α : Type} (point : PPoint α) (newX : α) : PPoint α :=
{ point with x := newX }
Lean은 뒤의 인수에서 α의 값을 추론할 수 있으므로 Nat을 명시적으로 제공하지 않고 natOrigin과 함께 사용할 수 있다.
#eval replaceX natOrigin 5
마찬가지로 length를 원소 타입을 암시적으로 받도록 다시 정의할 수 있다:
def length {α : Type} (xs : List α) : Nat :=
match xs with
| [] => 0
| y :: ys => Nat.succ (length ys)
이 length 함수는 primesUnder10에 직접 적용할 수 있다:
#eval length primesUnder10
표준 라이브러리에서 Lean은 이 함수를 List.length라고 부르므로, 구조체 필드에 사용하는 점 표기로 목록의 길이도 구할 수 있다:
#eval primesUnder10.length
C#과 Java에서 때때로 타입 인수를 명시적으로 제공해야 하듯이 Lean도 암시적 인수를 항상 찾을 수 있는 것은 아니다.
이 경우 인수의 이름을 사용해 제공할 수 있다.
예를 들어 정수 목록에서만 작동하는 List.length 버전은 α를 Int로 설정해 지정할 수 있다:
#check List.length (α := Int)1.6.3. More Built-In Datatypes
목록 외에도 Lean 표준 라이브러리에는 다양한 문맥에서 사용할 수 있는 여러 구조체와 귀납적 데이터 타입이 있다.
1.6.3.1. Option
모든 목록에 첫 원소가 있는 것은 아니다. 어떤 목록은 비어 있다. 컬렉션에 대한 많은 연산은 찾으려는 것을 찾지 못할 수 있다. 예를 들어 목록의 첫 원소를 찾는 함수가 그런 원소를 찾지 못할 수 있다. 따라서 첫 원소가 없음을 알리는 방법이 있어야 한다.
많은 언어에는 값이 없음을 나타내는 null 값이 있다.
Lean은 기존 타입에 특별한 null 값을 붙이는 대신 Option이라는 데이터 타입을 제공해 다른 타입에 값이 없음을 나타내는 표지를 붙인다.
예를 들어 null을 허용하는 Int는 Option Int로, null을 허용하는 문자열 목록은 Option (List String) 타입으로 나타낸다.
null 가능성을 나타내는 새 타입을 도입하면 타입 시스템이 null 검사를 잊지 않게 한다. Option Int는 Int가 필요한 문맥에서 사용할 수 없기 때문이다.
Option에는 some과 none이라는 두 생성자가 있으며 각각 기반 타입의 null이 아닌 버전과 null 버전을 나타낸다.
null이 아닌 생성자 some은 기반 값을 담고 none은 인수를 받지 않는다:
inductive Option (α : Type) : Type where
| none : Option α
| some (val : α) : Option α
Option 타입은 C#이나 Kotlin의 null 가능 타입과 매우 비슷하지만 동일하지는 않다.
이 언어에서 어떤 타입(예를 들어 Boolean)이 항상 실제 타입 값(true, false)을 가리킨다면 Boolean?이나 Nullable<Boolean> 타입은 null 값도 허용한다.
이를 타입 시스템에서 추적하면 매우 유용하다. 타입 검사기와 다른 도구가 프로그래머에게 null 검사를 기억하게 도울 수 있고, 타입 서명으로 null 가능성을 명시적으로 설명하는 API가 그렇지 않은 API보다 더 많은 정보를 제공한다.
하지만 이런 null 가능 타입은 한 가지 중요한 점에서 Lean의 Option과 다르다. 선택 가능성을 여러 층으로 만들 수 없다는 점이다.
Option (Option Int)는 none, some none, some (some 360)으로 만들 수 있다.
반면 Kotlin은 T??를 T?와 동등하게 취급한다.
이 미묘한 차이는 실제로 거의 중요하지 않지만 때때로 문제가 될 수 있다.
목록의 첫 원소가 있으면 List.head?를 사용해 찾을 수 있다.
물음표는 이름의 일부이며 C#이나 Kotlin에서 null 가능 타입을 나타내는 물음표와는 관계가 없다.
List.head?의 정의에서는 밑줄을 목록의 꼬리를 나타내는 데 사용한다.
패턴에서 밑줄은 무엇이든 매칭하지만 매칭한 데이터를 참조할 변수를 도입하지는 않는다.
이름 대신 밑줄을 사용하면 입력 일부를 무시한다는 점을 독자에게 명확히 전달할 수 있다.
def List.head? {α : Type} (xs : List α) : Option α :=
match xs with
| [] => none
| y :: _ => some y
Lean의 명명 관례에서는 실패할 수 있는 연산을 Option을 반환하는 버전에 ?, 잘못된 입력을 받으면 중단하는 버전에 !, 실패할 때 기본값을 반환하는 버전에 D 접미사를 붙여 묶어 정의한다.
이 관례에 따라 List.head는 호출자가 목록이 비어 있지 않다는 수학적 증거를 제공해야 하고, List.head?는 Option을 반환하며, List.head!는 빈 목록을 받으면 프로그램을 중단하고, List.headD는 목록이 비었을 때 반환할 기본값을 받는다.
물음표와 느낌표는 특별한 문법이 아니라 이름의 일부다. Lean의 명명 규칙이 많은 언어보다 자유롭기 때문이다.
head?는 List 네임스페이스에 정의되어 있으므로 접근자 표기로 사용할 수 있다:
#eval primesUnder10.head?하지만 빈 목록에서 이를 시험하면 두 오류가 발생한다:
#eval [].head?
이는 Lean이 표현식의 타입을 완전히 결정하지 못했기 때문이다.
특히 List.head?의 암시적 타입 인수와 List.nil의 암시적 타입 인수를 모두 찾지 못했다.
Lean의 출력에서 ?m.XYZ는 추론할 수 없는 프로그램의 일부를 나타낸다.
이러한 미지의 부분을 메타변수라고 하며 일부 오류 메시지에 나타난다.
표현식을 평가하려면 Lean이 타입을 찾을 수 있어야 하는데, 빈 목록에는 타입을 알아낼 원소가 없으므로 타입을 사용할 수 없었다.
타입을 명시적으로 제공하면 Lean이 계속 진행할 수 있다:
#eval [].head? (α := Int)타입 주석으로 타입을 제공할 수도 있다:
#eval ([] : List Int).head?오류 메시지는 유용한 단서를 제공한다. 두 메시지가 누락된 암시적 인수를 설명할 때 같은 메타변수를 사용한다. 즉 Lean이 실제 해의 값은 결정하지 못했지만 두 누락 부분이 같은 해를 공유한다고 결정했다는 뜻이다.
1.6.3.2. Prod
“Product”의 약자인 Prod 구조체는 두 값을 하나로 묶는 일반적인 방법이다.
예를 들어 Prod Nat String에는 Nat과 String이 들어 있다.
다시 말해 PPoint Nat을 Prod Nat Nat으로 바꿀 수 있다.
Prod는 C#의 튜플, Kotlin의 Pair·Triple 타입, C++의 tuple과 매우 비슷하다.
많은 애플리케이션에서는 Point처럼 단순한 경우에도 고유한 구조체를 정의하는 편이 좋다. 영역 용어를 사용하면 코드를 읽기 쉬워지기 때문이다.
또한 구조체 타입을 정의하면 서로 다른 영역 개념에 다른 타입을 부여해 뒤섞이지 않게 하므로 더 많은 오류를 잡을 수 있다.
반면 새 타입을 정의하는 비용을 감수할 가치가 없는 경우도 있다. 또한 어떤 라이브러리는 충분히 일반적이어서 “쌍”보다 더 구체적인 개념이 없기도 하다. 마지막으로 표준 라이브러리에는 내장 쌍 타입을 쉽게 다루게 해 주는 여러 편의 함수가 들어 있다.
Prod 구조체는 두 타입 인수로 정의한다:
structure Prod (α : Type) (β : Type) : Type where
fst : α
snd : β
1.6.3.3. Sum
Sum 데이터 타입은 서로 다른 두 타입의 값 중 하나를 선택하게 하는 일반적인 방법이다.
예를 들어 Sum String Int는 String이거나 Int다.
Prod와 마찬가지로 Sum은 매우 일반적인 코드를 작성할 때, 적절한 영역별 타입이 없는 아주 작은 코드 부분에서, 또는 표준 라이브러리에 유용한 함수가 있을 때 사용해야 한다.
대부분의 경우에는 사용자 정의 귀납 타입을 사용하는 편이 읽기 쉽고 유지 보수하기 좋다.
Sum α β 타입의 값은 α 타입의 값에 inl 생성자를 적용한 것이거나 β 타입의 값에 inr 생성자를 적용한 것이다:
inductive Sum (α : Type) (β : Type) : Type where
| inl : α → Sum α β
| inr : β → Sum α β
이 이름은 각각 “왼쪽 주입”과 “오른쪽 주입”의 약어다.
Prod에 데카르트 곱 표기를 사용하듯이 Sum에는 “동그라미 더하기” 표기를 사용하므로 α ⊕ β는 Sum α β를 쓰는 또 다른 방법이다.
Sum.inl과 Sum.inr에는 특별한 문법이 없다.
예를 들어 반려동물 이름이 개 이름이거나 고양이 이름일 수 있다면 문자열의 합으로 그 타입을 도입할 수 있다:
def PetName : Type := String ⊕ String
실제 프로그램에서는 보통 이 목적에 맞는 사용자 정의 귀납 데이터 타입을 의미 있는 생성자 이름과 함께 정의하는 편이 좋다.
여기서는 Sum.inl을 개 이름에, Sum.inr을 고양이 이름에 사용한다.
이 생성자를 사용해 동물 이름 목록을 쓸 수 있다:
def animals : List PetName :=
[Sum.inl "Spot", Sum.inr "Tiger", Sum.inl "Fifi",
Sum.inl "Rex", Sum.inr "Floof"]
두 생성자를 구분하기 위해 패턴 매칭을 사용할 수 있다.
예를 들어 동물 이름 목록에서 개의 수(Sum.inl 생성자의 수)를 세는 함수는 다음과 같다:
def howManyDogs (pets : List PetName) : Nat :=
match pets with
| [] => 0
| Sum.inl _ :: morePets => howManyDogs morePets + 1
| Sum.inr _ :: morePets => howManyDogs morePets
함수 호출은 중위 연산자보다 먼저 평가되므로 howManyDogs morePets + 1은 (howManyDogs morePets) + 1과 같다.
예상대로 #eval howManyDogs animals는 3을 낸다.
1.6.3.4. Unit
Unit은 unit이라는 인수를 받지 않는 생성자 하나만 있는 타입이다.
다시 말해 아무 인수도 적용하지 않은 그 생성자로 이루어진 단 하나의 값만 설명한다.
Unit은 다음과 같이 정의한다:
inductive Unit : Type where
| unit : Unit
Unit만으로는 특별히 유용하지 않다.
하지만 다형적 코드에서는 없는 데이터를 대신하는 자리표시자로 사용할 수 있다.
예를 들어 다음 귀납적 데이터 타입은 산술 표현식을 나타낸다:
inductive ArithExpr (ann : Type) : Type where
| int : ann → Int → ArithExpr ann
| plus : ann → ArithExpr ann → ArithExpr ann → ArithExpr ann
| minus : ann → ArithExpr ann → ArithExpr ann → ArithExpr ann
| times : ann → ArithExpr ann → ArithExpr ann → ArithExpr ann
타입 인수 ann은 주석을 나타내며 각 생성자에 주석이 붙는다.
파서에서 나온 표현식에는 소스 위치 주석을 붙일 수 있으므로 반환 타입을 ArithExpr SourcePos로 하면 파서가 각 하위 표현식에 SourcePos를 넣었음을 보장한다.
하지만 파서에서 나오지 않은 표현식에는 소스 위치가 없으므로 타입을 ArithExpr Unit으로 할 수 있다.
또한 모든 Lean 함수는 인수를 가지므로 다른 언어의 인수 없는 함수는 Unit 인수를 받는 함수로 표현할 수 있다.
반환 위치에서 Unit 타입은 C에서 파생된 언어의 void와 비슷하다.
C 계열에서 void를 반환하는 함수는 호출자에게 제어를 돌려주지만 흥미로운 값은 반환하지 않는다.
의도적으로 흥미롭지 않은 값인 Unit을 사용하면 타입 시스템에 특수 목적의 void 기능을 요구하지 않고 이를 표현할 수 있다.
Unit의 생성자는 빈 괄호로 쓸 수 있다: () : Unit.
1.6.3.5. Empty
Empty 데이터 타입에는 생성자가 전혀 없다.
따라서 어떤 호출의 연속도 Empty 타입의 값으로 끝날 수 없으므로 도달할 수 없는 코드를 나타낸다.
Empty는 Unit만큼 자주 사용되지 않는다.
하지만 일부 특수한 문맥에서는 유용하다.
많은 다형적 데이터 타입은 모든 생성자에서 타입 인수를 전부 사용하지 않는다.
예를 들어 Sum.inl과 Sum.inr은 각각 Sum의 타입 인수 중 하나만 사용한다.
Sum의 타입 인수 중 하나로 Empty를 사용하면 프로그램의 특정 지점에서 생성자 하나를 배제할 수 있다.
이를 통해 추가 제한이 있는 문맥에서도 일반 코드를 사용할 수 있다.
1.6.3.6. Naming: Sums, Products, and Units
일반적으로 생성자가 여러 개인 타입을 합 타입이라고 하고, 생성자 하나가 여러 인수를 받는 타입을 곱 타입이라고 한다.
이 용어는 일반 산술에서 사용하는 합과 곱에 대응한다.
관련된 타입이 유한한 수의 값을 가질 때 이 관계를 가장 쉽게 볼 수 있다.
α와 β가 각각 서로 다른 값 n개와 k개를 포함하는 타입이라면, α ⊕ β는 서로 다른 값 n + k개를 포함하고 α × β는 서로 다른 값 n \times k개를 포함한다.
예를 들어 Bool은 true, false라는 두 값을 갖고 Unit은 Unit.unit이라는 한 값을 갖는다.
곱 Bool × Unit은 (true, Unit.unit), (false, Unit.unit)이라는 두 값을 갖고, 합 Bool ⊕ Unit은 Sum.inl true, Sum.inl false, Sum.inr Unit.unit이라는 세 값을 갖는다.
마찬가지로 2 \times 1 = 2이고 2 + 1 = 3이다.
1.6.4. Messages You May Meet
정의할 수 있는 모든 구조체나 귀납 타입이 Type 타입을 가질 수 있는 것은 아니다.
특히 생성자가 임의의 타입을 인수로 받으면 귀납 타입은 다른 타입을 가져야 한다.
이런 오류는 보통 “우주 수준”에 관한 내용을 말한다.
예를 들어 다음 귀납 타입을 보자:
inductive MyType : Type where
| ctor : (α : Type) → α → MyTypeLean은 다음 오류를 낸다:
뒤의 장에서 그 이유와 정의를 작동하게 수정하는 방법을 설명한다. 지금은 타입을 생성자의 인수가 아니라 귀납 타입 전체의 인수로 만들어 보라.
마찬가지로 생성자의 인수가 정의 중인 데이터 타입을 인수로 받는 함수라면 정의가 거부된다. 예를 들어:
inductive MyType : Type where
| ctor : (MyType → Int) → MyType다음 메시지가 나온다:
기술적인 이유로 이런 데이터 타입을 허용하면 Lean의 내부 논리를 훼손할 수 있어 정리 증명기로 사용할 수 없게 될 수 있다.
매개변수를 둘 받는 재귀 함수는 쌍에 대해 매칭하지 말고 각 매개변수를 독립적으로 매칭해야 한다. 그렇지 않으면 재귀 호출이 더 작은 값에 대해 이루어지는지 확인하는 Lean의 메커니즘이 입력값과 재귀 호출의 인수 사이 연결을 볼 수 없다. 예를 들어 두 목록의 길이가 같은지 결정하는 다음 함수는 거부된다:
def sameLength (xs : List α) (ys : List β) : Bool :=
match (xs, ys) with
| ([], []) => true
| (x :: xs', y :: ys') => sameLength xs' ys'
| _ => false오류 메시지는 다음과 같다:
중첩 패턴 매칭으로 문제를 해결할 수 있다:
def sameLength (xs : List α) (ys : List β) : Bool :=
match xs with
| [] =>
match ys with
| [] => true
| _ => false
| x :: xs' =>
match ys with
| y :: ys' => sameLength xs' ys'
| _ => false다음 절에서 설명할 동시 매칭도 이 문제를 해결하는 또 다른 방법이며, 더 우아한 경우가 많다.
귀납 타입의 인수를 잊어도 혼란스러운 메시지가 나올 수 있다.
예를 들어 ctor의 타입에서 MyType에 α 인수를 전달하지 않으면:
inductive MyType (α : Type) : Type where
| ctor : α → MyTypeLean은 다음 오류로 응답한다:
오류 메시지는 MyType의 타입인 Type → Type 자체가 타입을 설명하지 않는다는 뜻이다.
MyType이 실제 타입이 되려면 인수가 필요하다.
정의의 타입 서명처럼 다른 문맥에서 타입 인수를 생략해도 같은 메시지가 나올 수 있다:
inductive MyType (α : Type) : Type where
| ctor : α → MyType αdef ofFive : MyType := ctor 5
다형적 타입을 사용하는 표현식을 평가하면 Lean이 값을 표시할 수 없는 상황이 발생할 수 있다.
#eval 명령은 주어진 표현식을 평가하며, 표현식의 타입을 사용해 결과를 어떻게 표시할지 결정한다.
함수와 같은 일부 타입에서는 이 과정이 실패하지만, 대부분의 다른 타입에 대해서는 Lean이 표시 코드를 자동으로 생성할 수 있다.
예를 들어 WoodSplittingTool에 특정 표시 코드를 제공할 필요가 없다:
inductive WoodSplittingTool where
| axe
| maul
| froe#eval WoodSplittingTool.axe
하지만 여기서 Lean이 사용하는 자동화에는 한계가 있다.
allTools는 세 도구 모두를 담은 목록이다:
def allTools : List WoodSplittingTool := [
WoodSplittingTool.axe,
WoodSplittingTool.maul,
WoodSplittingTool.froe
]이를 평가하면 오류가 발생한다:
#eval allTools
이는 Lean이 내장 테이블의 코드를 사용해 목록을 표시하려고 하기 때문이다. 그런데 이 코드는 WoodSplittingTool의 표시 코드가 이미 존재해야 한다고 요구한다.
정의의 끝에서 #eval의 일부로 표시 코드를 생성하게 하는 대신 데이터 타입을 정의할 때 이 코드를 생성하도록 Lean에 지시하면 이 오류를 우회할 수 있다. 정의에 deriving Repr를 추가하면 된다:
inductive Firewood where
| birch
| pine
| beech
deriving Repr
Firewood 목록을 평가하면 성공한다:
def allFirewood : List Firewood := [
Firewood.birch,
Firewood.pine,
Firewood.beech
]#eval allFirewood1.6.5. Exercises
-
리스트의 마지막 원소를 찾는 함수를 작성하라. 결과는
Option이어야 한다. -
주어진 술어를 만족하는 리스트의 첫 원소를 찾는 함수를 작성하라. 정의를
def List.findFirst? {α : Type} (xs : List α) (predicate : α → Bool) : Option α := …로 시작하라. -
쌍의 두 필드를 서로 바꾸는
Prod.switch함수를 작성하라. 정의를def Prod.switch {α β : Type} (pair : α × β) : β × α := …로 시작하라. -
PetName예제를 사용자 정의 데이터 타입을 사용하도록 다시 작성하고Sum을 사용하는 버전과 비교하라. -
두 리스트를 쌍의 리스트로 결합하는
zip함수를 작성하라. 결과 리스트의 길이는 두 입력 중 짧은 리스트와 같아야 한다. 정의를def zip {α β : Type} (xs : List α) (ys : List β) : List (α × β) := …로 시작하라. -
리스트의 처음
n개 원소를 반환하는 다형적 함수take를 작성하라. 여기서n은Nat이다. 리스트에n개보다 적은 원소가 있으면 결과 리스트는 입력 리스트 전체여야 한다.#eval take 3 ["bolete", "oyster"]는["bolete", "oyster"]를,#eval take 1 ["bolete", "oyster"]는["bolete"]를 내야 한다. -
타입과 산술의 유사성을 이용해 곱을 합 위로 분배하는 함수를 작성하라. 다시 말해 그 타입은
α × (β ⊕ γ) → (α × β) ⊕ (α × γ)이어야 한다. -
타입과 산술의 유사성을 이용해 2를 곱하는 연산을 합으로 바꾸는 함수를 작성하라. 다시 말해 그 타입은
Bool × α → α ⊕ α이어야 한다.