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