-
5.3 타입클래스프로그래밍 언어 속 타입 2022. 5. 17. 15:30
이 글은 인사이트 출판사의 제안으로 작성 중인 책 『프로그래밍 언어 속 타입』 원고의 일부입니다.
사과가 든 비닐봉지
처르지: 기계가 하나 줄은 거 같네?
큐리 박사: 맞아. “과즙듬뿍카레자동조리기계”를 없앴거든. 사람들이 생각보다 과즙 들어간 카레를 별로 안 좋아하더라고. 어차피 쓸 일도 별로 없는 주제에 자리만 차지하는 게 싫어서 그냥 없애 버렸지. 그래서 이제 다시 “사과깍둑카레자동조리기계”랑 “복숭아깍둑카레자동조리기계”만 남았어.
처르지: 그랬구나. 아쉽네.
큐리 박사: 뭐, 이 정도는 괜찮아. 맛있는 카레를 만들기 위해서는 다양한 시행착오가 필요한 법이니깐. 그보다 오늘 재밌는 일이 있었는데 들어 볼래?
처르지: 오, 좋지. 뭔데?
큐리 박사: 아침에 손님이 한 명 왔었어. 그러더니 직원한테 가서 이렇게 말하더라고. “저기, 이 사과가 든 비닐봉지로 카레를 만들어 주시겠어요?” 당연히 직원은 못하겠다고 했지. 내가 직원한테 사과나 복숭아만 사용할 수 있다고 가르쳤으니까.처르지: 비닐봉지에서 사과를 꺼내서 “사과깍둑카레자동조리기계”에 넣으면 되는 거 아니야?
큐리 박사: 정확해. 그래서 내가 직원을 불러서 이야기했지. 네가 어떻게 사용해야 하는지 아는 게 든 비닐봉지를 받거든, 그걸 열고 안에 있는 걸 꺼내서 카레를 만들라고. 어때? 좋은 방법이지?
처르지: 훌륭한걸? 그러면 이제 직원에게 사과가 든 비닐봉지를 갖다 주면 “사과깍둑카레자동조리기계”로 카레를 만들어 주겠네?
큐리 박사: 물론이지. 또, 여전히 이상한 과일을 갖다 주면 안 된다고 알려 줄 거라고. 그게 비닐봉지에 들어 있든 아니든 상관없어. 이를테면 수박이 든 비닐봉지를 갖다 줘 봤자 수박으로 카레를 만들지는 못하니까 받지 않을 거야.
처르지: 정말 그러겠네. 어, 재밌는 생각이 하나 떠올랐어. 이렇게 비닐봉지에 사과를 넣어서 직원에게 갖다 주면 카레를 만들어 주잖아? 그 말은, 직원이 사과가 든 비닐봉지도 어떻게 사용하는지 안다고 볼 수 있겠지?
큐리 박사: 그렇지.
처르지: 그러면 이 사과가 든 비닐봉지를 다시 다른 비닐봉지에 넣어서 직원에게 갖다 주면 어떨까? 이걸로도 카레를 만들어 줄까?
큐리 박사: 음, 좋은 질문이야. 직접 해 보지 그래?처르지: 그럴까? 저기, 이 사과가 든 비닐봉지가 든 비닐봉지로 카레를 만들어 주시겠어요?
직원: 네. 잠시만요. 여기 있습니다.
처르지: 오, 카레를 만들어 줬어!
큐리 박사: 정말이네!
지금까지 오버로딩에 의한 다형성을 서브타입에 의한 다형성과 함께 사용할 때 생기는 일들을 보았다. 이번에는 오버로딩에 의한 다형성을 매개변수에 의한 다형성과 함께 사용하는 방법을 볼 차례다.
4장에서 각 클래스에 두 값을 비교해 순서를 결정하는 gt 메서드를 정의했다.
class Person extends Comparable<Person> { ... Boolean gt(Person that) { return this.age > that.age; } }
sort 함수는 gt 메서드를 호출함으로써 리스트의 원소들을 비교해 리스트를 정렬한다.
void sort<T <: Comparable<T>>(List<T> lst) { ... if (lst[i].gt(lst[j])) { ... } ... }
gt를 메서드로 구현하는 것도 한 방법이지만, 그 대신 함수 오버로딩을 사용할 수도 있다. 다음처럼 여러 타입에 대해 gt 함수를 정의할 수 있다.
Boolean gt(Int v1, Int v2) { return v1 > v2; } Boolean gt(String v1, String v2) { ... } Boolean gt(Person v1, Person v2) { ... }
그러면 sort 함수도 gt 메서드 대신 gt 함수를 호출하도록 바꿔야 한다.
void sort<T>(List<T> lst) { ... if (gt(lst[i], lst[j])) { ... } ... }
물론 위 코드는 타입 검사를 통과하지 못한다. T는 아무 타입이나 나타낼 수 있으니 T 타입의 값이 gt의 인자로 사용될 수 있다는 보장이 없기 때문이다.
비슷한 문제를 4장에서도 겪었다. 4장에서의 해결 방안은 Comparable이라는 추상 클래스를 정의한 뒤 Comparable<T>를 T의 상한으로 지정하는 것이었다.abstract class Comparable<T> { Boolean gt(T that); } void sort<T <: Comparable<T>>(List<T> lst) { ... }
Comparable<T>는 T 타입의 값을 인자로 받는 gt 메서드를 가지고 있는 타입을 나타낸다. 따라서 T의 상한이 Comparable<T>라는 것은 T 타입의 값이 T 타입의 값을 인자로 받는 gt 메서드를 가져야 한다는 사실을 표현한다.
이번에도 유사한 개념이 필요하다. 우리가 원하는 것은 매개변수 타입이 (T, T)인 gt 함수가 있어야 한다는 조건을 표현하는 것이다. 이는 지금까지 본 개념만으로는 할 수 없는 일이다. 추상 클래스는 특정 타입이 어떤 메서드를 가진다는 사실을 표현한다. 하지만 특정 타입을 위한 함수가 존재한다는 사실을 표현할 수는 없다. 그러니 새로운 개념이 필요하다. 이럴 때 필요한 게 바로 타입클래스typeclass다.
타입클래스는 특정 타입을 위한 어떤 함수가 존재한다는 사실을 표현한다. “타입클래스”라는 용어에 “클래스”가 포함되기는 하지만 타입클래스는 클래스와 관련이 없다. 타입클래스는 클래스가 아니며 타입을 나타내는 클래스는 더더욱 아니다. 다만 타입이 만족해야 하는 조건을 표현한다는 점에서 추상 클래스와 비슷한 역할을 한다. 타입클래스를 정의할 때는 정의하려는 타입클래스의 이름, 타입 매개변수, 함수의 목록을 명시해야 한다. 다음 코드는 Comparable이라는 타입클래스를 정의한다.typeclass Comparable<T> { Boolean gt(T v1, T v2); }
이 코드의 뜻은 “어떤 타입 T가 Comparable 타입클래스에 속하려면 매개변수 타입이 (T, T)이고 결과 타입이 Boolean인 함수 gt가 있어야 한다”는 뜻이다.
T는 아무렇게나 고른 이름이니 T 대신 다른 이름을 사용해도 뜻하는 바는 변하지 않는다. 가령 T 대신 S를 사용할 수 있다.
typeclass Comparable<S> { Boolean gt(S v1, S v2); }
또한, Comparable 타입클래스에 속하기 위해 필요한 함수는 하나뿐이지만, 여러 함수를 요구하는 타입클래스를 정의할 수도 있다.
특정 타입을 어떤 타입클래스에 속하게 만들고 싶다면 타입클래스 인스턴스typeclass instance를 정의해야 한다. 타입클래스 인스턴스를 정의할 때는 해당 타입과 타입클래스의 이름을 명시한 뒤 타입클래스가 요구하는 함수를 모두 정의하면 된다. 예를 들어 다음 코드는 Int가 Comparable에 속하도록 만드는 타입클래스 인스턴스를 정의한다.instance Comparable<Int> { Boolean gt(Int v1, Int v2) { return v1 > v2; } }
이 코드는 “타입 Int가 Comparable 타입클래스에 속하며, 타입클래스 Comparable이 요구하는 함수 gt는 v1 > v2를 계산해 반환한다”는 뜻이다. 비슷한 방식으로 다른 타입 역시 Comparable에 속하게 만들 수 있다. 다음 코드는 String과 Person이 Comparable에 속하게 만든다.
instance Comparable<String> { Boolean gt(String v1, String v2) { ... } } instance Comparable<Person> { Boolean gt(Person v1, Person v2) { ... } }
타입클래스 인스턴스에 정의된 함수는 그냥 오버로딩된 함수처럼 사용할 수 있다. 따라서 다음과 같은 코드가 가능하다.
gt(1, 2); gt("a", "b"); gt(Person(...), Person(...));
그러니 타입클래스 인스턴스를 정의하는 것은 함수를 오버로딩하는 것과 거의 같다. 다만 지금 정의하는 함수가 어느 타입을 무슨 타입클래스에 속하게 만들기 위한 것인지 명시해야 한다는 차이가 있을 뿐이다.
이제 sort를 정의하기 위한 준비가 모두 끝났다. sort 함수는 타입 T가 Comparable에 속하는 경우에 한해서 List<T> 타입의 리스트를 인자로 받는다. 이를 코드로 표현하면 다음과 같다.void sort<T>(List<T> lst) requires Comparable<T> { ... if (gt(lst[i], lst[j])) { ... } ... }
매개변수 목록 뒤에 requires Comparable<T>를 덧붙임으로써 T가 Comparable에 속해야 한다는 조건을 달았다. 따라서 시그니처가 Boolean gt(T v1, T v2)인 함수가 존재한다는 사실을 알고 있는 상태에서 타입 검사기가 sort의 몸통을 검사한다. 그러므로 gt(lst[i], lst[j])가 검사를 통과하며, gt가 반환한 값을 if에 사용하는 것 역시 검사를 통과한다.
List<T> 타입의 리스트를 sort에 인자로 넘기려면 T가 Comparable에 속해야 한다. Int, String, Person 모두 Comparable에 속하므로 다음 코드가 타입 검사를 통과한다.sort<Int>(List<Int>(4, 1, 2, 5, 3)); sort<String>(List<String>("b", "c", "e", "d", "a")); sort<Person>(List<Person>(...));
반면, Vector를 위한 Comparable 타입클래스의 인스턴스를 따로 정의하지 않는 한, Vector가 Comparable에 속하지 않기 때문에 다음 코드는 타입 검사를 통과하지 못한다.
sort<Vector>(List<Vector>(...));
지금까지의 내용만으로 보면 타입클래스라는 개념이 굳이 필요할까 싶다. 오버로딩된 함수를 다루는 데 유용하기는 하지만, 애초에 함수 오버로딩을 사용해야만 할까? 그냥 이전처럼 그냥 다 메서드로 정의한 뒤 추상 클래스를 타입 매개변수의 상한으로 지정하는 것만으로도 충분해 보인다. 과연 추상 클래스에는 없는 타입클래스만의 장점이 있을까? 그렇다. 타입클래스에는 추상 클래스보다 개발자를 더 편하게 해 주는 장점들이 있다.
첫째, 함수는 아무 때나 정의할 수 있지만 메서드는 클래스를 정의할 때만 정의할 수 있다. 이게 뭔 장점인가 싶을 수 있다. 하지만 이는 매우 중요한 장점이다. 우리는 프로그램을 작성할 때 대부분 라이브러리를 사용한다. 그리고 라이브러리에 있는 코드는 수정할 수 없다. 그렇기에 메서드는 클래스를 정의할 때만 정의할 수 있다는 점이 치명적인 문제를 일으킨다.
Person 클래스가 라이브러리에 정의되어 있다고 하자. Person 클래스가 우리가 원하는 gt 메서드를 이미 가지고 있을 가능성은 별로 없다. gt 메서드 자체는 어려울 게 하나도 없지만, Person 클래스에 메서드를 추가하려면 클래스를 수정해야 한다. 하지만 Person이 라이브러리에 정의되어 있으니 불가능한 일이다. 그러니 사람의 리스트를 sort 함수를 사용해 정렬하고 싶어도 그럴 수 없다. sort 함수도 이미 준비되어 있고 gt를 구현하는 것도 쉬운데 메서드를 추가할 수 없는 게 문제라니 참으로 답답한 일이다. 언어가 구조에 의한 서브타입을 제공한다 하더라도 문제는 전혀 해결되지 않는다. 추상 클래스를 사용하든 구조를 드러내는 타입을 사용하든 클래스에 메서드를 추가할 수 없다는 사실은 변함없기 때문이다.
반면, 타입클래스에는 이런 문제가 없다. 함수는 아무 때나 정의할 수 있기 때문이다. Person이 라이브러리에 정의되어 있는 게 전혀 문제되지 않는다. 그냥 Person 타입을 위한 gt 함수를 작성함으로써 Person을 Comparable에 속하게 만들면 끝이다. 곧바로 사람의 리스트를 sort를 사용해 정렬할 수 있게 된다. 이처럼 라이브러리에 정의된 타입을 특정 추상 클래스의 서브타입으로 만드는 것은 일반적으로 불가능하지만, 특정 타입클래스에 속하게 만들기는 매우 쉽다. 이게 바로 타입클래스의 가장 큰 장점이다.
두 번째 장점은 제네릭 타입을 다룰 때 드러난다. 추상 클래스로는 특정 타입 인자를 받은 제네릭 타입만이 만족하는 성질을 표현하기 어렵지만, 타입클래스로는 쉽게 가능하다. 타입클래스의 장점이 드러나는 예시를 보기에 앞서, 추상 클래스든 타입클래스든 다 잘 작동하는 비교적 간단한 경우부터 보자.
리스트들로 구성된 리스트를 정렬해 보겠다. 이때, 두 리스트 사이의 순서는 리스트의 길이에 따라 결정된다. 더 짧은 리스트가 더 앞에 오도록 정렬하는 게 목표다. 길이는 모든 리스트가 가지는 속성이므로, 리스트를 구성하는 원소가 무엇이든 리스트끼리 비교할 수 있다. 이를 추상 클래스로 표현하면 다음과 같다.class List<T> extends Comparable<List<T>> { ... Boolean gt(List<T> that) { return this.length > that.length; } }
각 List<T>가 Comparable<List<T>>의 서브타입이므로 T가 무슨 타입이든 List<T> 타입의 리스트끼리 비교할 수 있다는 사실을 나타낸다. 즉, List<Int> 타입의 리스트끼리 비교할 수 있고, List<String> 타입의 리스트끼리 비교할 수 있다.
그렇다면 타입클래스로는 같은 사실을 어떻게 표현할 수 있을까? 쉽게 시도할 수 있는 방법은 타입클래스 인스턴스를 다음과 같이 여럿 정의하는 것이다.instance Comparable<List<Int>> { Boolean gt(List<Int> v1, List<Int> v2) { return v1.length > v2.length; } } instance Comparable<List<String>> { Boolean gt(List<String> v1, List<String> v2) { return v1.length > v2.length; } }
이렇게 하면 List<Int>와 List<String>이 Comparable에 속하게 된다. 같은 방식으로 다른 리스트 타입도 Comparable에 속하게 할 수 있을 것이다. 물론 썩 마음에 드는 방법은 아니다. 수없이 많은 타입이 있는데 각각을 위해 개별적인 타입클래스 인스턴스를 정의하기도 힘들고, v1.length > v2.length라는 코드가 반복되는 것도 별로다.
우리는 분명 이전에 비슷한 문제를 겪었다. 가령 3장에서 choose 함수를 정의할 때 각 타입마다 함수를 따로 정의하느라 같은 코드를 반복해서 적어야 했다.Int choose(Int v1, Int v2) { ... } String choose(String v1, String v2) { ... }
이를 해결하는 방법은 매개변수에 의한 다형성이었다. 타입 매개변수를 가진 제네릭 함수로서 choose를 정의하면 함수를 한번만 정의해도 충분했다.
T choose<T>(T v1, T v2) { ... }
이번에도 똑같다. 타입 매개변수를 사용하여 한번에 여러 타입을 특정 타입클래스에 속하게 만들 수 있다. 다음과 같이 말이다.
instance <T> Comparable<List<T>> { Boolean gt(List<T> v1, List<T> v2) { return v1.length > v2.length; } }
이 코드는 “각각의 타입 T마다, List<T>가 Comparable에 속한다”는 뜻이다. 즉, List<Int>도 Comparable에 속하고, List<String>도 Comparable에 속하는 셈이다. 따라서 T가 무슨 타입이든 List<T> 타입의 리스트끼리 비교할 수 있다.
지금까지는 추상 클래스를 사용하든 타입클래스를 사용하든 별 차이가 없다. 하지만 이제부터 볼 조금 더 복잡한 예시에서는 타입클래스만의 장점이 드러난다. 이번에도 리스트들로 구성된 리스트를 정렬해 보겠다. 단, 정렬 기준이 전과는 다르다. 마치 사전이 두 단어 사이의 순서를 정할 때 첫 글자부터 비교하듯이, 각 리스트에 들어있는 원소를 첫 번째 원소부터 차례대로 비교하여 두 리스트 사이의 순서를 정한다. 즉, 첫 원소끼리 서로 다르면 두 원소의 비교 결과가 곧 두 리스트의 비교 결과가 되고, 첫 원소가 같으면 그 다음 원소로 넘어간다. 가령 1이 2보다 작으니 List(1, 3)은 List(2, 1)보다 앞에 오고, 1끼리는 서로 같지만 2가 3보다 작으니 List(1, 2)가 List(1, 3)보다 앞에 온다. 즉, List(List(1, 3), List(2, 1), List(1, 2))를 정렬하면 List(List(1, 2), List(1, 3), List(2, 1))이 된다. 한편, 벡터의 비교는 정의한 적이 없으니 List<Vector> 타입의 리스트 역시 비교할 수 없다. 따라서 List<List<Vector>> 타입의 리스트를 정렬하는 것은 불가능하다. 정리하면, List<T> 타입의 리스트를 비교하기 위해서는 T 타입의 값을 비교할 수 있어야 한다.
이 조건을 추상 클래스로 표현하려고 하면 문제가 생긴다. 일단, 다음과 같이 코드를 작성하면 타입 검사를 통과할 수 없다.class List<T> extends Comparable<List<T>> { ... Boolean gt(List<T> that) { this[i].gt(that[i]) ... } }
위 코드에서 this[i]의 타입은 T인데 T는 아무 타입이나 될 수 있다. 따라서 this[i]가 gt라는 메서드를 가지고 있다는 보장이 없다. 그러니 코드를 다음처럼 고치는 수밖에 없다.
class List<T <: Comparable<T>> extends Comparable<List<T>> { ... Boolean gt(List<T> that) { this[i].gt(that[i]) ... } }
이 코드는 타입 검사를 통과한다. T가 Comparable<T>의 서브타입이니 this[i]가 gt 메서드를 가진다는 사실이 보장되기 때문이다. 하지만 이 코드는 심각한 문제를 일으킨다. 이제 T가 Comparable<T>의 서브타입일 때만 List<T> 타입의 리스트를 만들 수 있다. 정수의 리스트는 만들 수 있어도 벡터의 리스트는 만들 수 없는 것이다. 우리가 원하는 것은 정수의 리스트는 정렬할 수 있도록 하되 벡터의 리스트는 정렬할 수 없게 하는 것이지, 벡터의 리스트를 아예 만들지 못하게 하는 것은 아니다. 정렬을 할 수 없더라도 리스트는 다양한 용도로 필요하다. 그러니 벡터의 리스트를 만들 수 없게 된 것은 큰 문제다.
우리가 표현하고 싶은 것은 List<T> 중 일부만 비교 가능하다는 사실이다. 하지만 추상 클래스를 사용할 때는 이런 “중간” 선택지가 없다. List 클래스를 정의할 때 Comparable을 상속함으로써 모든 List<T>를 비교할 수 있도록 만들거나, Comparable을 상속하지 않음으로써 모든 List<T>를 비교할 수 없게 만드는 두 가지만 가능하다. 전자를 선택하면 서로 비교 가능한 값들로만 리스트를 만들 수 있게 되고, 후자를 선택하면 리스트끼리 아예 비교할 수 없게 되니, 어느 쪽도 마음에 들지 않는다.
타입클래스는 이런 문제를 깔끔하게 해결한다. Comparable에 속하는 각각의 T마다 List<T>를 Comparable에 속하게 만드는 타입클래스 인스턴스를 다음과 같이 한번에 정의할 수 있다.instance <T> Comparable<List<T>> requires Comparable<T> { Boolean gt(List<T> v1, List<T> v2) { gt(v1[i], v2[i]) ... } }
이 코드는 “각각의 타입 T마다, List<T>가 Comparable에 속한다. 단, T가 Comparable에 속할 때만”이라는 뜻으로 이해할 수 있다.
이제 Int, String, Person이 Comparable에 속하므로 List<Int>, List<String>, List<Person> 역시 Comparable에 속한다. 따라서 List<List<Int>>, List<List<String>>, List<List<Person>> 타입의 리스트를 sort를 사용해 정렬할 수 있다. 더 나아가, List<Int>, List<String>, List<Person>이 Comparable에 속하므로 List<List<Int>>, List<List<String>>, List<List<Person>> 역시 Comparable에 속한다. 그러니 List<List<List<Int>>>, List<List<List<String>>>, List<List<List<Person>>> 타입의 리스트조차도 정렬할 수 있게 된다. 몇 번이고 이 과정을 반복할 수 있음은 물론이다. 가령 List<List<List<List<List<List<List<List<List<Int>>>>>>>>> 타입의 리스트도 정렬할 수 있다. 물론 현실적으로 이런 리스트를 사용하는 경우는 없겠지만. 한편, Vector가 Comparable에 속하지 않기에 List<Vector> 역시 Comparable에 속하지 않고, List<Vector>와 List<List<Vector>> 타입의 리스트는 정렬할 수 없다. 하지만 여전히 List<Vector>와 List<List<Vector>> 타입의 리스트를 만드는 것은 문제없이 가능하다.
이처럼 추상 클래스로는 제네릭 타입이 타입 인자에 상관없이 항상 만족하는 성질만 표현할 수 있다. 하지만 타입클래스로는 항상 만족하는 성질은 물론이고 특정 타입 인자를 받은 경우에만 만족하는 성질 역시 표현할 수 있다. 이것이 타입클래스의 또 다른 장점이다.
러스트
trait Comparable { fn gt(v1: &Self, v2: &Self) -> bool; }
타입클래스를 정의할 때는 trait이라는 키워드를 사용한다. 또한, 타입 매개변수를 명시적으로 선언하는 대신, 이 타입클래스에 속하게 될 타입을 Self라고 항상 부른다.
impl Comparable for i32 { fn gt(v1: &i32, v2: &i32) -> bool { v1 > v2 } } impl Comparable for String { fn gt(v1: &String, v2: &String) -> bool { v1 > v2 } }
타입클래스 인스턴스를 정의할 때는 impl이라는 키워드를 사용한다.
Comparable::gt(&1, &2); Comparable::gt(&"a".to_string(), &"b".to_string());
타입클래스에 정의한 함수를 호출할 때는 타입클래스 이름을 함께 적는다.
fn sort<T: Comparable>(lst: Vec<T>) -> () { if Comparable::gt(&lst[...], &lst[...]) { ... } } sort::<i32>(vec![2, 3, 1]); sort::<String>(vec!["b".to_string(), "c".to_string(), "a".to_string()]);
타입 T가 타입클래스 C에 속한다는 조건을 T: C라고 쓴다.
impl<T> Comparable for Vec<T> { fn gt(v1: &Vec<T>, v2: &Vec<T>) -> bool { v1.len() > v2.len() } } sort::<Vec<i32>>(vec![vec![1, 2], vec![1, 2, 3], vec![1]]);
각각의 타입 T마다 Vec<T>를 Comparable에 속하게 만들 수 있다.
impl<T: Comparable> Comparable for Vec<T> { fn gt(v1: &Vec<T>, v2: &Vec<T>) -> bool { if Comparable::gt(&v1[...], &v2[...]) { ... } } } sort::<Vec<i32>>(vec![vec![1, 2], vec![1, 2, 3], vec![1]]); sort::<Vec<Vec<i32>>>(vec![]);
Comparable에 속하는 각각의 타입 T마다 Vec<T>를 Comparable에 속하게 만들 수 있다.
스칼라
trait Comparable[T]: def gt(v1: T, v2: T): Boolean
타입클래스를 정의할 때는 trait이라는 키워드를 사용한다.
given Comparable[Int] with def gt(v1: Int, v2: Int): Boolean = v1 > v2 given Comparable[String] with def gt(v1: String, v2: String): Boolean = v1 > v2
타입클래스 인스턴스를 정의할 때는 given이라는 키워드를 사용한다.
summon[Comparable[Int]].gt(1, 2) summon[Comparable[String]].gt("a", "b")
타입클래스 C에 정의한 함수 f를 타입 A를 위해 호출할 때는 summon[C[A]].f라고 적는다. 단, 많은 경우에 [C[A]] 부분은 생략한 채 summon.f라고 적을 수 있다.
def sort[T: Comparable](lst: List[T]): Unit = if summon.gt(lst(...), lst(...)) then ... sort[Int](List(2, 3, 1)) sort[String](List("b", "c", "a"))
타입 T가 타입클래스 C에 속한다는 조건을 T: C라고 쓴다.
given [T]: Comparable[List[T]] with def gt(v1: List[T], v2: List[T]): Boolean = v1.length > v2.length sort[List[Int]](List(List(1, 2), List(1, 2, 3), List(1)))
각각의 타입 T마다 List[T]를 Comparable에 속하게 만들 수 있다.
given [T: Comparable]: Comparable[List[T]] with def gt(v1: List[T], v2: List[T]): Boolean = if summon.gt(v1(...), v2(...)) then ... sort[List[Int]](List()) sort[List[List[Int]]](List())
Comparable에 속하는 각각의 타입 T마다 List[T]를 Comparable에 속하게 만들 수 있다.
하스켈
class Comparable a where gt :: a -> a -> Bool
타입클래스를 정의할 때는 class라는 키워드를 사용한다.
instance Comparable Int where gt v1 v2 = v1 > v2 instance Comparable String where gt v1 v2 = v1 > v2
타입클래스 인스턴스를 정의할 때는 instance라는 키워드를 사용한다.
gt (1 :: Int) 2 gt "a" "b"
타입클래스에 정의한 함수를 함수 이름만으로 호출할 수 있다.
sort :: Comparable a => [a] -> () sort lst = if gt (lst !! ...) (lst !! ...) then ... else ... sort [2 :: Int, 3, 1] sort ["b", "c", "a"]
타입 a가 타입클래스 C에 속한다는 조건을 C a =>라고 쓴다.
instance Comparable [a] where gt v1 v2 = (length v1) > (length v2) sort [[1 :: Int, 2], [1, 2, 3], [1]]
각각의 타입 a마다 [a]를 Comparable에 속하게 만들 수 있다.
instance Comparable a => Comparable [a] where gt v1 v2 = if gt (v1 !! ...) (v2 !! ...) then ... else ... sort [[1 :: Int, 2], [1, 2, 3], [1]] sort [[[1 :: Int]]]
Comparable에 속하는 각각의 타입 a마다 [a]를 Comparable에 속하게 만들 수 있다.
'프로그래밍 언어 속 타입' 카테고리의 다른 글
마치며 (2) 2022.05.17 5.4 카인드 (0) 2022.05.17 5.2 메서드 오버라이딩 - 메서드 오버라이딩과 결과 타입 (0) 2022.05.17 5.2 메서드 오버라이딩 - 메서드 선택의 한계 (0) 2022.05.17 5.2 메서드 오버라이딩 (0) 2022.05.17