Lean으로 배우는 함수형 프로그래밍

3.8. 요약🔗

3.8.1. 타입 클래스와 오버로딩🔗

타입 클래스는 함수와 연산자를 오버로딩하기 위한 Lean의 메커니즘입니다. 다형 함수는 여러 타입에 사용할 수 있지만, 어떤 타입에 사용하든 동일한 방식으로 동작합니다. 예를 들어, 두 리스트를 이어 붙이는 다형 함수는 리스트 원소의 타입이 무엇이든 사용할 수 있지만, 구체적으로 어떤 타입이 주어졌는지에 따라 다르게 동작할 수는 없습니다. 반면에 타입 클래스로 오버로딩된 연산 역시 여러 타입에 사용할 수 있습니다. 하지만 각 타입은 오버로드된 연산에 대한 자체 구현을 필요로 합니다. 이는 어떤 타입이 주어지느냐에 따라 동작이 달라질 수 있음을 의미합니다.

타입 클래스는 이름, 매개변수, 그리고 타입이 지정된 여러 이름으로 이루어진 본문을 가집니다. 이름은 오버로드된 연산을 가리키는 수단이고, 매개변수는 정의의 어떤 측면을 오버로드할 수 있는지를 결정하며, 본문은 오버로드 가능한 연산의 이름과 타입 시그니처를 제공합니다. 각 오버로드 가능한 연산을 타입 클래스의 메서드라고 합니다. 타입 클래스는 일부 메서드의 기본 구현을 다른 메서드를 이용해 제공할 수 있으므로, 구현자는 필요하지 않은 경우 각 오버로드를 일일이 직접 정의하지 않아도 됩니다.

타입 클래스의 인스턴스는 주어진 매개변수에 대한 메서드 구현을 제공합니다. 인스턴스는 다형적일 수 있으며, 이 경우 다양한 매개변수에 대해 동작할 수 있습니다. 또한 특정 타입에 대해 더 효율적인 버전이 있는 경우에는 기본 메서드의 더 구체적인 구현을 선택적으로 제공할 수도 있습니다.

타입 클래스 매개변수는 입력 매개변수(기본값)이거나 출력 매개변수(outParam 수식어로 표시)입니다. Lean은 모든 입력 매개변수가 더 이상 메타변수가 아니게 될 때까지 인스턴스 탐색을 시작하지 않지만, 출력 매개변수는 인스턴스를 탐색하는 도중에 결정될 수 있습니다. 타입 클래스의 매개변수가 반드시 타입일 필요는 없으며, 일반적인 값일 수도 있습니다. 자연수 리터럴을 오버로드하는 데 사용되는 OfNat 타입 클래스는 오버로드되는 Nat 자체를 매개변수로 받으므로, 인스턴스가 허용되는 수를 제한할 수 있습니다.

인스턴스에는 @[default_instance] 속성을 붙일 수 있습니다. 기본 인스턴스로 지정된 인스턴스는 타입에 메타변수가 있어서 Lean이 인스턴스를 찾지 못하게 되는 경우에 대체 수단으로 선택됩니다.

3.8.2. 공통 문법을 위한 타입 클래스🔗

Lean의 중위 연산자 대부분은 타입 클래스를 통해 오버라이드됩니다. 예를 들어 덧셈 연산자는 Add라는 타입 클래스에 대응합니다. 이러한 연산자 대부분에는 두 인자의 타입이 같지 않아도 되는 이종(heterogeneous) 버전이 있습니다. 이러한 이종 연산자는 HAdd처럼 이름이 H로 시작하는 버전의 클래스를 사용하여 오버로드됩니다.

인덱싱 문법은 GetElem이라는 타입 클래스를 통해 오버로드되며, 이 타입 클래스에는 증명이 관여합니다. GetElem에는 출력 매개변수가 두 개 있습니다. 하나는 컬렉션에서 꺼낼 원소의 타입이고, 다른 하나는 인덱스 값이 컬렉션의 범위 안에 있다는 증거로 무엇을 인정할지 결정하는 데 쓸 수 있는 함수입니다. 이 증거는 명제로 기술되며, 배열 인덱싱이 사용되면 Lean은 이 명제를 증명하려고 시도합니다. Lean이 컴파일 시점에 리스트나 배열 접근 연산이 범위 안에 있는지 확인할 수 없는 경우, 인덱싱 문법에 ?를 덧붙여 그 확인을 런타임으로 미룰 수 있습니다.

3.8.3. 펑터🔗

펑터는 매핑 연산을 지원하는 다형 타입입니다. 이 매핑 연산은 다른 구조는 전혀 바꾸지 않고 모든 원소를 “제자리에서” 변환합니다. 예를 들어, 리스트는 펑터이며, 매핑 연산은 리스트의 항목을 빠뜨리거나 중복시키거나 뒤섞어서는 안 됩니다.

펑터는 map을 가진다는 점으로 정의되지만, Lean의 Functor 타입 클래스에는 기본 메서드가 하나 더 있습니다. 이 메서드는 값에 상수 함수를 매핑하는 역할을 하며, 다형 타입 변수로 주어진 타입을 가진 모든 값을 같은 새 값으로 바꿉니다. 일부 펑터에서는 전체 구조를 순회하는 것보다 더 효율적으로 이 작업을 수행할 수 있습니다.

3.8.4. 인스턴스 파생하기🔗

많은 타입 클래스에는 매우 표준적인 구현이 있습니다. 예를 들어 불리언 동등성 클래스 BEq는 보통 먼저 두 인자가 같은 생성자로 만들어졌는지 확인한 다음, 그 생성자의 인자들이 모두 같은지 확인하는 방식으로 구현됩니다. 이러한 클래스의 인스턴스는 자동으로 만들 수 있습니다.

귀납적 타입이나 구조체를 정의할 때 선언 끝에 deriving 절을 붙이면 인스턴스가 자동으로 생성됩니다. 또한 deriving instance ... for ... 명령을 사용하면 데이터 타입 정의 바깥에서도 인스턴스를 생성할 수 있습니다. 인스턴스를 파생할 수 있는 클래스는 각각 별도의 처리가 필요하므로 모든 클래스를 파생할 수 있는 것은 아닙니다.

3.8.5. 강제 변환🔗

강제 변환을 사용하면 Lean은 데이터를 한 타입에서 다른 타입으로 바꾸는 함수 호출을 삽입하여, 원래라면 컴파일 타임 오류가 되었을 상황을 복구할 수 있습니다. 예를 들어, 임의의 타입 α에서 Option α 타입으로의 강제 변환 덕분에 some 생성자를 쓰지 않고 값을 직접 쓸 수 있습니다. 이로써 Option은 객체 지향 언어의 nullable 타입과 더 비슷하게 동작합니다.

강제 변환에는 여러 종류가 있습니다. 각 강제 변환은 서로 다른 종류의 오류로부터 복구할 수 있으며, 각각 고유한 타입 클래스로 표현됩니다. Coe 클래스는 타입 오류로부터 복구하는 데 사용됩니다. Lean은 β 타입을 기대하는 문맥에서 α 타입의 표현식을 만나면, 먼저 α를 β로 변환할 수 있는 강제 변환들을 하나의 사슬로 엮으려고 시도하며, 이것이 불가능할 때에만 오류를 표시합니다. CoeDep 클래스는 강제 변환되는 특정 값을 추가 매개변수로 받으므로, 그 값에 대해 추가적인 타입 클래스 탐색을 수행하거나 인스턴스에서 생성자를 사용하여 변환의 범위를 제한할 수 있습니다. CoeFun 클래스는 함수 적용을 컴파일할 때 원래라면 "함수가 아님" 오류가 발생했을 상황을 가로채어, 가능한 경우 함수 위치에 있는 값을 실제 함수로 변환할 수 있게 합니다.