3. 오버로딩과 타입 클래스
많은 언어에서 내장 데이터 타입은 특별한 취급을 받습니다. 예를 들어, C와 Java에서 +는 float와 int를 더하는 데 사용할 수 있지만, 서드파티 라이브러리의 임의 정밀도 숫자를 더하는 데는 사용할 수 없습니다. 마찬가지로, 숫자 리터럴은 내장 타입에는 직접 사용할 수 있지만 사용자 정의 숫자 타입에는 사용할 수 없습니다. 다른 언어들은 연산자에 대한 오버로딩(overloading) 메커니즘을 제공하는데, 여기서는 동일한 연산자에 새로운 타입에 대한 의미를 부여할 수 있습니다. C++와 C# 같은 이러한 언어에서는 다양한 내장 연산자를 오버로딩할 수 있으며, 컴파일러는 타입 검사기를 사용하여 특정 구현을 선택합니다.
숫자 리터럴과 연산자뿐만 아니라, 많은 언어에서 함수나 메서드의 오버로딩도 허용합니다. C++, Java, C#, Kotlin에서는 인자의 개수와 타입이 서로 다른 메서드의 여러 구현을 허용합니다. 컴파일러는 인수의 개수와 타입을 사용하여 어떤 오버로드가 의도되었는지 판단합니다.
함수와 연산자 오버로딩에는 핵심적인 제약이 있습니다: 다형 함수는 자신의 타입 인자를 특정 오버로드가 존재하는 타입으로 제한할 수 없습니다. 예를 들어, 오버로드된 메서드는 문자열, 바이트 배열, 파일 포인터에 대해 정의될 수 있지만, 이들 중 어느 것에도 작동하는 두 번째 메서드를 작성할 방법은 없습니다. 대신, 이 두 번째 메서드 자체도 원래 메서드의 오버로드를 가진 각 타입마다 오버로드되어야 하므로, 단일한 다형적 정의 대신 수많은 상용구 정의가 생겨나게 됩니다. 이러한 제약의 또 다른 결과로, (Java의 동등 비교와 같은) 일부 연산자는 굳이 그럴 필요가 없는 경우에도 인자의 모든 조합에 대해 정의되고 맙니다. 프로그래머가 세심한 주의를 기울이지 않으면, 이로 인해 프로그램이 실행 중에 충돌하거나 조용히 잘못된 결과를 계산할 수 있습니다.
Lean은 연산자, 함수, 리터럴의 오버로딩을 다형성과 잘 어울리는 방식으로 허용하는, Haskell에서 처음 도입된 타입 클래스라는 메커니즘을 사용하여 오버로딩을 구현합니다. 타입 클래스는 오버로드 가능한 연산들의 모음을 기술합니다. 새로운 타입에 대해 이러한 연산을 오버로드하려면 새로운 타입에 대한 각 연산의 구현을 포함하는 instance를 생성합니다. 예를 들어, Add라는 이름의 타입 클래스는 덧셈을 허용하는 타입들을 기술하며, Nat에 대한 Add의 인스턴스는 Nat에 대한 덧셈의 구현을 제공합니다.
class와 instance라는 용어는 객체 지향 언어에 익숙한 사람들에게 혼란스러울 수 있는데, 이는 객체 지향 언어의 클래스 및 인스턴스와 밀접한 관련이 없기 때문입니다. 하지만 두 개념이 공통된 뿌리를 공유하는 것은 사실입니다. 일상 언어에서 “클래스”라는 용어는 몇 가지 공통된 속성을 공유하는 집단을 가리킵니다. 객체 지향 프로그래밍에서 클래스는 확실히 공통 속성을 가진 객체들의 그룹을 나타내지만, 이 용어는 그러한 그룹을 기술하기 위한 프로그래밍 언어의 특정 메커니즘을 추가로 가리킵니다. 타입 클래스 또한 공통된 속성(즉, 특정 연산의 구현)을 공유하는 타입을 기술하는 수단이지만, 객체 지향 프로그래밍에서 찾아볼 수 있는 클래스와는 사실상 그 외에 공통점이 없습니다.
Lean의 타입 클래스는 Java나 C#의 인터페이스에 훨씬 더 가깝습니다. 타입 클래스와 인터페이스는 모두 어떤 타입이나 여러 타입의 모음에 대해 구현되는, 개념적으로 서로 연관된 연산들의 집합을 기술합니다. 이와 마찬가지로, 타입 클래스의 인스턴스는 Java나 C# 클래스의 인스턴스라기보다는, 구현된 인터페이스가 규정하는 Java나 C# 클래스 내 코드에 가깝습니다. Java나 C#의 인터페이스와 달리, 타입에는 해당 타입의 작성자가 접근할 수 없는 타입 클래스에 대한 인스턴스도 부여될 수 있습니다. 이런 점에서 이들은 Rust의 trait와 매우 유사합니다.