Lean으로 하는 함수형 프로그래밍

1.6. 다형성🔗

대부분의 언어에서와 마찬가지로, Lean의 타입도 인자를 받을 수 있습니다. 예를 들어, List Nat 타입은 자연수의 리스트를 나타내고, List String은 문자열의 리스트를 나타내며, List (List Point)는 점의 리스트로 이루어진 리스트를 나타냅니다. 이는 C#이나 Java 같은 언어에서의 List<Nat>, List<String>, 또는 List<List<Point>>와 매우 유사합니다. Lean이 함수에 인자를 전달할 때 공백을 사용하는 것과 마찬가지로, 타입에 인자를 전달할 때도 공백을 사용합니다.

함수형 프로그래밍에서 다형성이라는 용어는 일반적으로 타입을 인자로 받는 데이터 타입과 정의를 가리킵니다. 이는 해당 용어가 일반적으로 상위 클래스의 일부 동작을 재정의할 수 있는 하위 클래스를 가리키는 객체 지향 프로그래밍 커뮤니티와는 다릅니다. 이 책에서 “다형성”은 항상 첫 번째 의미로 사용됩니다. 이러한 타입 인자는 데이터 타입이나 정의 내에서 사용될 수 있으며, 이를 통해 인자의 이름을 다른 타입으로 치환함으로써 얻어지는 모든 타입에 대해 동일한 데이터 타입이나 정의를 사용할 수 있습니다.

Point 구조체는 xy 필드가 모두 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을 인자로 제공하면 필드 xy가 타입 Nat을 갖는 구조체가 만들어지는데, 이는 인자 이름 α가 인자 타입 Nat으로 치환되었기 때문입니다. Lean에서 타입은 일반적인 표현식이므로, 다형 타입(예: PPoint)에 인자를 전달하는 데에는 특별한 구문이 필요하지 않습니다.

정의는 타입을 인자로 받을 수도 있는데, 이는 정의를 다형적으로 만듭니다. replaceX 함수는 PPointx 필드를 새 값으로 대체합니다. replaceX임의의 다형적 점(point)에 대해 작동할 수 있도록 하려면, 그 자체가 다형적이어야 합니다. 이는 첫 번째 인자를 점의 필드 타입으로 하고, 이후의 인자들이 첫 번째 인자의 이름을 다시 참조하도록 함으로써 달성됩니다.

def replaceX (α : Type) (point : PPoint α) (newX : α) : PPoint α := { point with x := newX }

다시 말해, 인자 pointnewX의 타입이 α를 언급할 때, 이는 첫 번째 인자로 제공된 타입이 무엇이든 그 타입을 가리키는 것입니다. 이는 함수 인자 이름이 함수 본문에서 나타날 때 제공된 값을 참조하는 방식과 유사합니다.

이는 Lean에게 replaceX의 타입을 확인하도록 요청한 다음, replaceX Nat의 타입을 확인하도록 요청함으로써 확인할 수 있습니다.

#check (replaceX)
replaceX : (α : Type)  PPoint α  α  PPoint α

이 함수 타입은 첫 번째 인자의 이름을 포함하며, 타입 내의 이후 인자들은 이 이름을 다시 참조합니다. 함수 적용의 값이 함수 본문에서 인자 이름을 제공된 인자 값으로 치환하여 구해지는 것처럼, 함수 적용의 타입도 함수의 반환 타입에서 인자의 이름을 제공된 값으로 치환하여 구해집니다. 첫 번째 인자인 Nat을 제공하면 타입의 나머지 부분에 등장하는 모든 αNat으로 치환됩니다:

#check replaceX Nat
replaceX Nat : PPoint Nat  Nat  PPoint Nat

나머지 인자들은 명시적으로 이름 붙여지지 않았기 때문에, 더 많은 인자가 제공되더라도 추가적인 치환은 일어나지 않습니다:

#check replaceX Nat natOrigin
replaceX Nat natOrigin : Nat  PPoint Nat
#check replaceX Nat natOrigin 5
replaceX Nat natOrigin 5 : PPoint Nat

전체 함수 적용 표현식의 타입이 타입을 인자로 전달함으로써 결정되었다는 사실은 이를 평가할 수 있는지 여부와는 아무런 관련이 없습니다.

#eval replaceX Nat natOrigin 5
{ x := 5, y := 0 }

다형 함수는 이름이 붙은 타입 인자를 받고, 이후의 타입들이 그 인자의 이름을 참조하는 방식으로 동작합니다. 하지만 타입 인자라고 해서 이름을 붙일 수 있게 해주는 특별한 점은 없습니다. 양수 또는 음수 부호를 나타내는 데이터 타입이 주어졌을 때:

inductive Sign where | pos | neg

인자로 부호(sign)를 받는 함수를 작성하는 것이 가능합니다. 인자가 양수이면 함수는 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를 예상 타입으로 만듭니다.

posOrNegThreepos에 적용하면, 함수의 본문과 반환 타입 양쪽 모두에서 인자 이름 spos로 치환됩니다. 평가는 표현식과 그 타입 양쪽 모두에서 일어날 수 있습니다:

(posOrNegThree Sign.pos : match Sign.pos with | Sign.pos => Nat | Sign.neg => Int)((match Sign.pos with | Sign.pos => (3 : Nat) | Sign.neg => (-3 : Int)) : match Sign.pos with | Sign.pos => Nat | Sign.neg => Int)((3 : Nat) : Nat)3

1.6.1. 연결 리스트🔗

Lean의 표준 라이브러리는 List라고 불리는 정규 연결 리스트 데이터 타입과, 이를 더 편리하게 사용할 수 있게 해 주는 특수 구문을 포함합니다. 리스트는 대괄호로 작성합니다. 예를 들어, 10보다 작은 소수를 포함하는 리스트는 다음과 같이 작성할 수 있습니다:

def primesUnder10 : List Nat := [2, 3, 5, 7]

내부적으로 List는 다음과 같이 정의된 귀납적 타입입니다:

inductive List (α : Type) where | nil : List α | cons : α List α List α

표준 라이브러리의 실제 정의는 아직 소개되지 않은 기능을 사용하기 때문에 약간 다르지만, 본질적으로는 유사합니다. 이 정의는 PPoint가 그랬던 것처럼, List가 단 하나의 타입을 인자로 받는다는 것을 나타냅니다. 이 타입은 리스트에 저장된 항목의 타입입니다. 생성자에 따르면, List αnil 또는 cons 중 하나로 만들어질 수 있습니다. 생성자 nil은 빈 목록을 나타내고, 생성자 cons는 비어 있지 않은 목록에 사용됩니다. cons의 첫 번째 인자는 리스트의 머리이고, 두 번째 인자는 그 꼬리입니다. n개의 항목을 포함하는 리스트는 n개의 cons 생성자를 포함하며, 그중 마지막 생성자는 꼬리로 nil을 가집니다.

primesUnder10 예제는 List의 생성자를 직접 사용하여 더 명시적으로 작성할 수 있습니다.

def explicitPrimesUnder10 : List Nat := List.cons 2 (List.cons 3 (List.cons 5 (List.cons 7 List.nil)))

이 두 정의는 완전히 동등하지만, primesUnder10explicitPrimesUnder10보다 읽기 훨씬 더 쉽습니다.

List를 소비하는 함수는 Nat을 소비하는 함수와 거의 같은 방식으로 정의할 수 있습니다. 실제로 연결 리스트를 생각하는 한 가지 방법은, 각 succ 생성자마다 추가 데이터 필드가 매달려 있는 Nat으로 보는 것입니다. 이 관점에서 볼 때, 리스트의 길이를 계산하는 것은 각 conssucc로, 마지막 nilzero로 치환하는 과정입니다. replaceX가 점의 필드 타입을 인자로 받았던 것과 마찬가지로, length는 리스트 항목의 타입을 받습니다. 예를 들어, 리스트가 문자열을 포함한다면 첫 번째 인자는 String입니다: length String ["Sourdough", "bread"]. 다음과 같이 계산되어야 합니다:

length String ["Sourdough", "bread"]length String (List.cons "Sourdough" (List.cons "bread" List.nil))Nat.succ (length String (List.cons "bread" List.nil))Nat.succ (Nat.succ (length String List.nil))Nat.succ (Nat.succ Nat.zero)2

length의 정의는 다형적(리스트 항목 타입을 인자로 받기 때문)이면서 동시에 재귀적(자기 자신을 참조하기 때문)입니다. 일반적으로 함수는 데이터의 형태를 따릅니다: 재귀적 데이터 타입은 재귀 함수로 이어지고, 다형적 데이터 타입은 다형 함수로 이어집니다.

def length (α : Type) (xs : List α) : Nat := match xs with | List.nil => Nat.zero | List.cons y ys => Nat.succ (length α ys)

xsys 같은 이름은 관례적으로 알 수 없는 값들의 리스트를 나타내는 데 사용됩니다. 이름에 붙은 s는 복수형임을 나타내므로, “x s”나 “y s”가 아니라 “exes”와 “whys”로 발음합니다.

리스트에 대한 함수를 더 읽기 쉽게 만들기 위해, 대괄호 표기법 []를 사용하여 nil에 대해 패턴 매칭할 수 있으며, 중위 연산자 ::cons 대신 사용할 수 있습니다:

def length (α : Type) (xs : List α) : Nat := match xs with | [] => 0 | y :: ys => Nat.succ (length α ys)

1.6.2. 암시적 인자🔗

replaceXlength 모두 사용하기가 다소 번거로운데, 타입 인자가 일반적으로 이후 값들에 의해 고유하게 결정되기 때문입니다. 실제로 대부분의 언어에서 컴파일러는 타입 인자를 스스로 완벽하게 결정할 수 있으며, 사용자의 도움이 필요한 경우는 가끔뿐입니다. 이는 Lean에서도 마찬가지입니다. 함수를 정의할 때 인자를 괄호 대신 중괄호로 감싸면 암시적으로 선언할 수 있습니다. 예를 들어, 암시적 타입 인자를 사용하는 replaceX 버전은 다음과 같습니다:

def replaceX {α : Type} (point : PPoint α) (newX : α) : PPoint α := { point with x := newX }

natOrigin과 함께 사용할 때 Nat을 명시적으로 제공하지 않아도 되는데, 이는 Lean이 이후 인자들로부터 α의 값을 추론할 수 있기 때문입니다:

{ x := 5, y := 0 }#eval replaceX natOrigin 5
{ x := 5, y := 0 }

마찬가지로, length도 항목 타입을 암시적으로 받도록 재정의할 수 있습니다:

def length {α : Type} (xs : List α) : Nat := match xs with | [] => 0 | y :: ys => Nat.succ (length ys)

length 함수는 primesUnder10에 직접 적용할 수 있습니다:

4#eval length primesUnder10
4

표준 라이브러리에서 Lean은 이 함수를 List.length라고 부르는데, 이는 구조체 필드 접근에 사용되는 점(dot) 구문을 리스트의 길이를 구하는 데에도 사용할 수 있음을 의미합니다:

4#eval primesUnder10.length
4

C#과 Java가 때때로 타입 인자를 명시적으로 제공하도록 요구하는 것과 마찬가지로, Lean도 암시적 인자를 항상 찾아낼 수 있는 것은 아닙니다. 이런 경우에는 이름을 사용하여 제공할 수 있습니다. 예를 들어, 정수 리스트에 대해서만 작동하는 List.length 버전은 αInt로 설정하여 지정할 수 있습니다:

List.length : List Int Nat#check List.length (α := Int)
List.length : List Int  Nat

1.6.3. 더 많은 내장 데이터 타입🔗

리스트 외에도, Lean의 표준 라이브러리에는 다양한 상황에서 사용할 수 있는 여러 다른 구조체와 귀납적 데이터 타입이 포함되어 있습니다.

1.6.3.1. Option🔗

모든 목록에 첫 번째 항목이 있는 것은 아닙니다. 즉, 어떤 목록은 비어 있습니다. 컬렉션에 대한 많은 연산은 찾고자 하는 것을 찾지 못할 수 있습니다. 예를 들어 목록에서 첫 번째 항목을 찾는 함수는 그러한 항목을 전혀 찾지 못할 수도 있습니다. 따라서 첫 번째 항목이 없었음을 알릴 방법이 있어야 합니다.

많은 언어에는 값의 부재를 나타내는 null 값이 있습니다. 기존 타입에 특별한 null 값을 부여하는 대신, Lean은 다른 타입에 값이 없음을 나타내는 지시자를 부여하는 Option이라는 데이터타입을 제공합니다. 예를 들어, null이 될 수 있는 IntOption Int로 표현되며, null이 될 수 있는 문자열 리스트는 Option (List String) 타입으로 표현됩니다. 널 가능성을 표현하는 새로운 타입을 도입한다는 것은 타입 시스템이 null 검사를 잊어버릴 수 없도록 보장한다는 의미입니다. 왜냐하면 Int가 기대되는 맥락에서 Option Int를 사용할 수 없기 때문입니다.

Option은 각각 기저 타입의 널이 아닌 버전과 널인 버전을 나타내는 somenone이라는 두 개의 생성자를 가지고 있습니다. 널이 아닌 생성자인 some은 기저 값을 포함하며, none은 인자를 받지 않습니다:

inductive Option (α : Type) : Type where | none : Option α | some (val : α) : Option α

Option 타입은 C#이나 Kotlin과 같은 언어의 널 허용 타입(nullable type)과 매우 유사하지만, 완전히 동일하지는 않습니다. 이러한 언어들에서 타입(예를 들어 Boolean)이 항상 해당 타입의 실제 값(truefalse)만을 가리킨다면, Boolean? 또는 Nullable<Boolean> 타입은 여기에 더해 null 값도 허용합니다. 이를 타입 시스템에서 추적하는 것은 매우 유용합니다: 타입 검사기와 다른 도구들은 프로그래머가 null을 확인해야 한다는 것을 기억하도록 도와줄 수 있으며, 타입 시그니처를 통해 널 가능성을 명시적으로 기술하는 API는 그렇지 않은 API보다 더 많은 정보를 제공합니다. 하지만 이러한 널 가능 타입은 매우 중요한 한 가지 측면에서 Lean의 Option과 다른데, 바로 여러 겹의 선택성을 허용하지 않는다는 점입니다. Option (Option Int)none, some none, 또는 some (some 360)으로 구성할 수 있습니다. 반면, Kotlin은 T??T?와 동등한 것으로 취급합니다. 이 미묘한 차이는 실제로는 거의 중요하지 않지만, 때때로 영향을 미칠 수 있습니다.

리스트에 첫 번째 항목이 존재하는 경우 이를 찾으려면 List.head?를 사용하십시오. 이 물음표는 이름의 일부이며, C#이나 Kotlin에서 널 허용 가능한 타입을 표시하기 위해 물음표를 사용하는 것과는 관련이 없습니다. 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 네임스페이스에 정의되어 있으므로, 접근자 표기법과 함께 사용할 수 있습니다:

some 2#eval primesUnder10.head?
some 2

하지만 빈 리스트에 대해 테스트를 시도하면 두 가지 오류가 발생합니다.

#eval don't know how to synthesize implicit argument `α` @_root_.List.head? ?m.3 [] context: Type ?u.3don't know how to synthesize implicit argument `α` @List.nil ?m.3 context: Type ?u.3[].head?
don't know how to synthesize implicit argument `α`
  @List.nil ?m.3
context:
Type ?u.3
don't know how to synthesize implicit argument `α`
  @_root_.List.head? ?m.3 []
context:
Type ?u.3

이는 Lean이 표현식의 타입을 완전히 결정할 수 없었기 때문입니다. 특히, List.head?의 암시적 타입 인자도, List.nil의 암시적 타입 인자도 찾을 수 없었습니다. Lean의 출력에서 ?m.XYZ는 추론할 수 없었던 프로그램의 일부를 나타냅니다. 이러한 알 수 없는 부분을 메타변수(metavariable)라고 부르며, 이는 일부 오류 메시지에도 등장합니다. 표현식을 평가하려면 Lean은 그 타입을 찾을 수 있어야 하는데, 빈 리스트에는 타입을 찾아낼 항목이 하나도 없기 때문에 타입을 찾을 수 없었습니다. 타입을 명시적으로 제공하면 Lean이 진행할 수 있습니다:

none#eval [].head? (α := Int)
none

타입 주석을 사용하여 타입을 제공할 수도 있습니다:

none#eval ([] : List Int).head?
none

오류 메시지는 유용한 단서를 제공합니다. 두 메시지는 누락된 암묵적 인자를 설명하기 위해 동일한 메타변수를 사용하는데, 이는 Lean이 해당 해의 실제 값을 결정할 수는 없었지만 두 누락된 부분이 하나의 해를 공유한다는 것은 판단했다는 것을 의미합니다.

1.6.3.2. Prod🔗

"Product"의 줄임말인 Prod 구조체는 두 값을 함께 결합하는 일반적인 방법입니다. 예를 들어, Prod Nat StringNatString을 포함합니다. 다시 말해, PPoint NatProd Nat Nat으로 대체할 수 있습니다. Prod는 C#의 튜플, 코틀린의 PairTriple 타입, C++의 tuple과 매우 유사합니다. 많은 애플리케이션의 경우, Point와 같이 단순한 경우에도 자체 구조체를 정의하는 것이 최선인데, 이는 도메인 용어를 사용하면 코드를 더 쉽게 읽을 수 있기 때문입니다. 또한, 구조체 타입을 정의하면 서로 다른 도메인 개념에 서로 다른 타입을 부여함으로써 개념이 뒤섞이는 것을 방지하여 더 많은 오류를 잡아낼 수 있습니다.

반면, 새로운 타입을 정의하는 오버헤드를 감수할 가치가 없는 경우도 있습니다. 또한, 일부 라이브러리는 충분히 일반적이어서 "쌍"보다 더 구체적인 개념이 존재하지 않습니다. 마지막으로, 표준 라이브러리에는 내장 쌍 타입을 더 쉽게 다룰 수 있도록 해주는 다양한 편의 함수가 포함되어 있습니다.

구조체 Prod는 두 개의 타입 인자를 사용하여 정의됩니다:

structure Prod (α : Type) (β : Type) : Type where fst : α snd : β

리스트는 매우 자주 사용되므로, 이를 더 읽기 쉽게 만들기 위한 특별한 문법이 존재합니다. 같은 이유로, 곱 타입과 그 생성자 모두 특별한 문법을 가지고 있습니다. Prod α β 타입은 집합의 데카르트 곱에 대한 일반적인 표기법을 본떠 일반적으로 α × β로 표기됩니다. 마찬가지로, 순서쌍에 대한 일반적인 수학 표기법을 Prod에도 사용할 수 있습니다. 다시 말해, 다음과 같이 작성하는 대신:

def fives : String × Int := { fst := "five", snd := 5 }

다음과 같이 작성하는 것으로 충분합니다:

def fives : String × Int := ("five", 5)

두 표기법 모두 오른쪽 결합적입니다. 이는 다음 정의들이 동등하다는 것을 의미합니다:

def sevens : String × Int × Nat := ("VII", 7, 4 + 3)def sevens : String × (Int × Nat) := ("VII", (7, 4 + 3))

다시 말해, 두 개보다 많은 타입의 모든 곱과 그에 대응하는 생성자는 실제로는 내부적으로 중첩된 곱과 중첩된 쌍입니다.

1.6.3.3. Sum🔗

Sum 데이터타입은 서로 다른 두 타입의 값 사이에서 선택할 수 있도록 하는 일반적인 방법입니다. 예를 들어, Sum String IntString이거나 Int입니다. Prod와 마찬가지로, Sum도 매우 일반적인 코드를 작성할 때, 적절한 도메인 특화 타입이 없는 매우 작은 코드 영역에서, 또는 표준 라이브러리에 유용한 함수가 있을 때 사용해야 합니다. 대부분의 상황에서는 커스텀 귀납적 타입을 사용하는 것이 가독성과 유지 보수성 면에서 더 낫습니다.

Sum α β 타입의 값은 α 타입의 값에 적용된 생성자 inl이거나 β 타입의 값에 적용된 생성자 inr입니다:

inductive Sum (α : Type) (β : Type) : Type where | inl : α Sum α β | inr : β Sum α β

이 이름들은 각각 "왼쪽 삽입"과 "오른쪽 삽입"의 약어입니다. Prod에 데카르트 곱 표기법이 사용되는 것과 마찬가지로, Sum에는 “원 플러스” 표기법이 사용되므로, α βSum α β를 쓰는 또 다른 방법입니다. Sum.inlSum.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과 같습니다. 예상대로, 3#eval howManyDogs animals3을 산출합니다.

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은 주석(annotation)을 나타내며, 각 생성자는 주석이 달려 있습니다. 파서에서 나온 표현식에는 소스 위치가 표시되어 있을 수 있으므로, ArithExpr SourcePos라는 반환 타입은 파서가 각 하위 표현식에 SourcePos를 넣었음을 보장합니다. 하지만 파서에서 나오지 않은 표현식은 소스 위치를 갖지 않으므로, 그 타입은 ArithExpr Unit이 될 수 있습니다.

또한, 모든 Lean 함수는 인자를 가지므로, 다른 언어에서의 인자가 없는 함수는 Unit 인자를 받는 함수로 표현할 수 있습니다. 반환 위치에서 Unit 타입은 C에서 파생된 언어의 void와 유사합니다. C 계열 언어에서 void를 반환하는 함수는 호출자에게 제어를 돌려주지만, 흥미로운 값은 전혀 반환하지 않습니다. 의도적으로 흥미롭지 않은 값이 됨으로써, Unit은 타입 시스템에 특수 목적의 void 기능을 요구하지 않고도 이를 표현할 수 있게 해줍니다. Unit의 생성자는 빈 괄호로 작성할 수 있습니다: () : Unit.

1.6.3.5. Empty🔗

Empty 데이터 타입은 생성자가 전혀 없습니다. 따라서 이는 도달할 수 없는 코드를 나타내는데, 이는 어떤 일련의 호출도 Empty 타입의 값으로 종료될 수 없기 때문입니다.

EmptyUnit만큼 자주 사용되지는 않습니다. 하지만 몇 가지 특수한 맥락에서는 유용합니다. 많은 다형적 데이터 타입은 모든 생성자에서 자신의 타입 인자를 전부 사용하지는 않습니다. 예를 들어, Sum.inlSum.inr은 각각 Sum의 타입 인자 중 하나만 사용합니다. EmptySum의 타입 인자 중 하나로 사용하면 프로그램의 특정 지점에서 생성자 중 하나를 배제할 수 있습니다. 이를 통해 제네릭 코드를 추가적인 제약이 있는 컨텍스트에서도 사용할 수 있습니다.

1.6.3.6. 명명: 합, 곱, 그리고 단위🔗

일반적으로 여러 개의 생성자를 제공하는 타입을 합 타입이라고 부르며, 단일 생성자가 여러 개의 인자를 받는 타입을 곱 타입이라고 부릅니다. 이 용어들은 일반적인 산술에서 사용되는 합과 곱과 관련이 있습니다. 이 관계는 관련된 타입이 유한한 개수의 값을 포함할 때 가장 쉽게 확인할 수 있습니다. αβ가 각각 n개와 k개의 서로 다른 값을 포함하는 타입이라면, α βn + k개의 서로 다른 값을 포함하고 α × βn \times k개의 서로 다른 값을 포함합니다. 예를 들어, Booltruefalse라는 두 개의 값을 가지고, UnitUnit.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. 마주칠 수 있는 메시지🔗

정의 가능한 모든 구조체나 귀납적 타입이 Type 타입을 가질 수 있는 것은 아닙니다. 특히, 어떤 생성자가 임의의 타입을 인자로 받는다면, 그 귀납적 타입은 다른 타입을 가져야 합니다. 이러한 오류는 대개 “유니버스 레벨”에 관한 내용을 언급합니다. 예를 들어, 다음 귀납적 타입에 대해서는:

inductive MyType : Type where Invalid universe level in constructor `MyType.ctor`: Parameter `α` has type Type at universe level 2 which is not less than or equal to the inductive type's resulting universe level 1| ctor : (α : Type) α MyType

Lean은 다음과 같은 오류를 표시합니다:

Invalid universe level in constructor `MyType.ctor`: Parameter `α` has type
  Type
at universe level
  2
which is not less than or equal to the inductive type's resulting universe level
  1

이후의 장에서는 왜 그런지, 그리고 정의가 작동하도록 수정하는 방법을 설명합니다. 지금은 타입을 생성자가 아니라 귀납적 타입 전체에 대한 인자로 만들어 보십시오.

마찬가지로, 생성자의 인자가 정의 중인 데이터 타입을 인자로 받는 함수라면 해당 정의는 거부됩니다. 예를 들어:

(kernel) arg #1 of 'MyType.ctor' has a non positive occurrence of the datatypes being declaredinductive MyType : Type where | ctor : (MyType Int) MyType

다음 메시지를 출력합니다:

(kernel) arg #1 of 'MyType.ctor' has a non positive occurrence of the datatypes being declared

기술적인 이유로, 이러한 데이터 타입을 허용하면 Lean의 내부 논리를 훼손할 수 있게 되어 정리 증명기로 사용하기에 부적합해질 수 있습니다.

매개변수 두 개를 받는 재귀 함수는 이를 쌍으로 묶어 매칭하지 말고, 각 매개변수를 독립적으로 매칭해야 합니다. 그렇지 않으면, 재귀 호출이 더 작은 값에 대해 이루어지는지 검사하는 Lean의 메커니즘이 입력 값과 재귀 호출의 인자 사이의 연관성을 파악하지 못합니다. 예를 들어, 두 리스트의 길이가 같은지 판단하는 다음 함수는 거부됩니다:

def fail to show termination for sameLength with errors failed to infer structural recursion: Not considering parameter α of sameLength: it is unchanged in the recursive calls Not considering parameter β of sameLength: it is unchanged in the recursive calls Cannot use parameter xs: failed to eliminate recursive application sameLength xs' ys' Cannot use parameter ys: failed to eliminate recursive application sameLength xs' ys' Could not find a decreasing measure. The basic measures relate at each recursive call as follows: (<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted) xs ys 1) 1816:28-46 ? ? Please use `termination_by` to specify a decreasing measure.sameLength (xs : List α) (ys : List β) : Bool := match (xs, ys) with | ([], []) => true | (x :: xs', y :: ys') => sameLength xs' ys' | _ => false

오류 메시지는 다음과 같습니다:

fail to show termination for
  sameLength
with errors
failed to infer structural recursion:
Not considering parameter α of sameLength:
  it is unchanged in the recursive calls
Not considering parameter β of sameLength:
  it is unchanged in the recursive calls
Cannot use parameter xs:
  failed to eliminate recursive application
    sameLength xs' ys'
Cannot use parameter ys:
  failed to eliminate recursive application
    sameLength xs' ys'


Could not find a decreasing measure.
The basic measures relate at each recursive call as follows:
(<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted)
              xs ys
1) 1816:28-46  ?  ?
Please use `termination_by` to specify a decreasing measure.

이 문제는 중첩 패턴 매칭을 통해 해결할 수 있습니다:

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 : α type expected, got (MyType : Type Type)MyType

Lean은 다음과 같은 오류로 응답합니다:

type expected, got
  (MyType : Type  Type)

오류 메시지는 MyType의 타입인 Type Type가 그 자체로는 타입을 나타내지 않는다는 것을 말하고 있습니다. MyType이 진정한 의미의 타입이 되려면 인자가 필요합니다.

정의의 타입 시그니처와 같이 다른 맥락에서 타입 인자가 생략된 경우에도 동일한 메시지가 나타날 수 있습니다:

inductive MyType (α : Type) : Type where | ctor : α MyType αdef ofFive : type expected, got (MyType : Type Type)MyType := ctor 5
type expected, got
  (MyType : Type  Type)

다형 타입을 사용하는 표현식을 평가하면 Lean이 값을 표시할 수 없는 상황이 발생할 수 있습니다. #eval 명령은 제공된 표현식을 평가하며, 표현식의 타입을 사용하여 결과를 표시하는 방식을 결정합니다. 함수와 같은 일부 타입에서는 이 과정이 실패하지만, 대부분의 다른 타입에 대해서는 Lean이 표시 코드를 완벽하게 자동으로 생성할 수 있습니다. 예를 들어, WoodSplittingTool에 대해 특정한 표시 코드를 Lean에 제공할 필요가 없습니다.

inductive WoodSplittingTool where | axe | maul | froeWoodSplittingTool.axe#eval WoodSplittingTool.axe
WoodSplittingTool.axe

하지만 여기서 Lean이 사용하는 자동화에는 한계가 있습니다. allTools는 세 도구 모두의 목록입니다:

def allTools : List WoodSplittingTool := [ WoodSplittingTool.axe, WoodSplittingTool.maul, WoodSplittingTool.froe ]

이를 평가하면 오류가 발생합니다:

Could not synthesize a `ToExpr`, `Repr`, or `ToString` instance for type List WoodSplittingTool#eval allTools
Could not synthesize a `ToExpr`, `Repr`, or `ToString` instance for type
  List WoodSplittingTool

이는 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 ][Firewood.birch, Firewood.pine, Firewood.beech]#eval allFirewood
[Firewood.birch, Firewood.pine, Firewood.beech]

1.6.5. 연습문제🔗

  • 리스트에서 마지막 항목을 찾는 함수를 작성하십시오. 이 함수는 Option을 반환해야 합니다.

  • 주어진 술어를 만족하는 리스트의 첫 번째 항목을 찾는 함수를 작성하십시오. def List.findFirst? {α : Type} (xs : List α) (predicate : α Bool) : Option α := 로 정의를 시작하십시오.

  • 쌍(pair)의 두 필드를 서로 맞바꾸는 함수 Prod.switch를 작성하십시오. 정의는 def Prod.switch {α β : Type} (pair : α × β) : β × α := 로 시작하십시오.

  • PetName 예제를 커스텀 데이터타입을 사용하도록 다시 작성하고, Sum을 사용하는 버전과 비교해 보십시오.

  • 두 리스트를 결합하여 쌍의 리스트로 만드는 함수 zip을 작성하십시오. 결과 리스트의 길이는 더 짧은 입력 리스트의 길이와 같아야 합니다. def zip {α β : Type} (xs : List α) (ys : List β) : List (α × β) := 로 정의를 시작하십시오.

  • 리스트에서 처음 n개의 항목을 반환하는 다형 함수 take를 작성하십시오. 여기서 nNat입니다. 리스트에 n개보다 적은 항목이 포함되어 있다면, 결과 리스트는 입력 리스트 전체가 되어야 합니다. #eval take 3 ["bolete", "oyster"]["bolete", "oyster"]를 산출해야 하고, ["bolete"]#eval take 1 ["bolete", "oyster"]["bolete"]를 산출해야 합니다.

  • 타입과 산술 사이의 유비를 이용해, 곱을 합에 대해 분배하는 함수를 작성하십시오. 다시 말해, 이 함수는 α × (β γ) (α × β) (α × γ) 타입을 가져야 합니다.

  • 타입과 산술 사이의 유비를 이용하여, 2를 곱하는 것을 합으로 바꾸는 함수를 작성하십시오. 다시 말해, Bool × α α α 타입을 가져야 합니다.