2.12. 트레잇-as-타입 금지
중요
트레잇 이름을 타입 표현식 자리에 그대로 적는 것을 금지합니다. 런타임에 구현 타입이 정해지는 값이 필요하면 비용이 문법에 드러나는 any Trait 을 적습니다 (2.12.3).
2.12.1. 금지 항목
let a: Number = 32i32; // ERROR - Number 는 트레잇
fn process(x: Display) -> str { ... } // ERROR
struct Holder { item: Iterator; } // ERROR
위 예시처럼 변수 타입, 매개변수 타입, 반환 타입, 구조체 필드 타입 어디든 트레잇 이름이 그대로 등장하면 컴파일 에러로 처리합니다.
2.12.2. 금지의 근거
금지 대상은 숨은 동적 디스패치입니다. 트레잇 이름을 타입 자리에 그대로 적는 형태는 겉보기에 평범한 정적 타입과 구별되지 않지만, 실제로는 런타임 디스패치를 만들어 냅니다. 이 디스패치는 적지 않은 비용을 동반합니다. 우선 실행 시점에 구현을 골라 호출하므로 분기 예측에 실패할 경우 직접 호출 대비 두 배 이상 느려질 수 있습니다. 또한 디스패치 자료에 대한 추가 메모리 접근이 필요하기 때문에 캐시 친화성도 떨어집니다. 무엇보다 컴파일러의 인라이닝과 최적화가 차단되어 버립니다.
과학·데이터 코드의 핵심 수치 루프가 이 비용을 모르는 사이에 떠안지 않도록, rok 은 맨 트레잇 이름을 타입으로 쓰는 것을 허용하지 않습니다. 런타임 다형성은 any 라고 적힌 자리에만 있습니다. any Trait 은 동적 디스패치 비용이 any 키워드로 문법에 드러나므로 허용합니다.
2.12.3. 다형성이 필요할 때. 세 대안
그렇다면 다형성이 필요한 경우에는 어떻게 해야 할까요. rok 은 세 가지 대안을 제공합니다.
1. 닫힌 후보가 있으면 합 타입
후보 집합이 미리 정해져 있다면 합 타입을 사용하면 됩니다.
let a: i32 | f32 | f64 = 32i32;
fn process(x: i32 | f32) -> str { ... }
이 방식은 후보가 컴파일 타임에 정해져 있을 때 적합합니다. 메모리와 디스패치 비용이 명확하게 드러나며 (합 타입 참고), 패턴 매칭의 완전성 검사도 가능합니다.
2. 열린 다형성이면 제네릭
반면 후보가 열려 있다면 제네릭을 활용합니다.
fn process<T: Display>(x: T) -> str { ... }
struct Holder<T: Iterator> { item: T; }
이 경우는 후보가 컴파일 타임에 결정되지 않거나 호출자마다 다를 때에 어울립니다. 단형화를 통해 직접 호출이 이루어지므로 비용은 0 에 가깝고 (제네릭 참고), 트레잇 바운드를 통해 메서드도 호출할 수 있습니다.
3. 런타임에 구현 타입이 정해지면 any Trait
한 슬롯이나 컬렉션에 서로 다른 구현 타입의 값이 섞여 담겨야 한다면 제약 붙은 any 를 사용합니다.
let shapes: List<any Shape> = [Circle { r: 1.0 }, Square { side: 2.0 }];
fn report(e: any Error) -> str { f"{e}" }
struct ConfigError { msg: str; cause: any Error | none; }
any Trait 슬롯에는 Trait 을 구현하는 타입의 값만 넣을 수 있으며 (컴파일 타임 검사), 그 값에서는 Trait 과 그 슈퍼트레잇의 메서드를 호출할 수 있습니다. 호출은 런타임 태그로 디스패치되며 비용 모델은 any 슬롯 과 같습니다. 정확한 규칙은 2.2.10 에서 정의합니다.
2.12.4. 결정 기준
세 대안 가운데 무엇을 고를지는 다음 기준을 따릅니다.
| 상황 | 선택 |
|---|---|
| 후보가 고정된 작은 집합 (3~5 개) | 합 타입 |
| 후보가 유저 / 라이브러리 확장 가능 | 제네릭 |
| 후보가 런타임에만 결정 (예: 플러그인, 이종 컬렉션) | any Trait |
| 어떤 제약도 없이 무엇이든 담아야 함 | any |
세 번째 경우, 즉 런타임에 타입이 정해지는 다형성이 필요할 때에는 any Trait 을 사용하면 됩니다. 동적 디스패치 비용이 생기지만, 그 비용이 any 라고 적힌 자리에 명시적으로 모인다는 특징이 있습니다.
2.12.5. 슈퍼트레잇과 바운드 표현은 허용
여기서 한 가지 분명히 해두고 싶은 점이 있습니다. 금지되는 것은 어디까지나 타입 위치에 등장하는 트레잇 이름이라는 사실입니다. 다음 사례들은 모두 정상적으로 허용됩니다.
trait Ord: Eq { ... } // 슈퍼트레잇 선언
fn f<T: Add + Display>(x: T) { ... } // 트레잇 바운드
fn g<T>(x: T) where T: Ord, T: Clone { ... } // where 절
impl Display for Point { ... } // impl 의 트레잇 자리
이러한 위치에서 트레잇 이름은 제약을 표현하는 것이지 타입을 가리키는 것이 아니기 때문입니다.
2.12.6. 컴파일 에러의 형태
마지막으로 실제 컴파일 에러가 어떤 모습으로 나타나는지 보여 줍니다. 트레잇 이름이 타입 위치에 등장하면 컴파일러는 다음과 비슷한 진단을 출력합니다.
ERROR: 'Display' is a trait, not a type.
To allow multiple types, use generics, a sum type, or `any Display`:
fn f<T: Display>(x: T) { ... }
fn f(x: SomeType | OtherType) { ... }
fn f(x: any Display) { ... }
이 진단은 사용자가 앞서 소개한 세 대안 중 하나를 선택하도록 자연스럽게 안내합니다.