rokdocs
v0.1

3.1. 연산자 디스패치

이 장에서는 구두점과 연산자 토큰 에 정의된 연산자 토큰이 트레잇 메서드 호출로 변환되는 일반 원칙을 정의합니다. 각 연산자가 어느 트레잇의 어느 메서드에 매핑되는지에 대한 구체적인 매트릭스는 표준 라이브러리에서 정의합니다.

3.1.1. 모든 연산자는 트레잇 메서드

언어와 표준 라이브러리의 경계 에서 정한 바와 같이, 연산자 구문은 언어가 제공하지만 그 동작은 표준 라이브러리의 트레잇이 결정합니다.

a + b         →   <typeof(a) as Add<typeof(b)>>::add(a, b)
a - b         →   <typeof(a) as Sub<typeof(b)>>::sub(a, b)
a * b         →   <typeof(a) as Mul<typeof(b)>>::mul(a, b)
a < b         →   <typeof(a) as Cmp<typeof(b)>>::lt(a, b)
-a            →   <typeof(a) as Neg>::neg(a)
!a            →   <typeof(a) as Not>::not(a)

이처럼 모든 연산자가 같은 패턴을 따르고 있습니다. 즉 언어가 토큰을 인식한 뒤, 이를 트레잇 메서드 호출로 변환하는 구조입니다.

3.1.2. 연산자 → 트레잇 매핑은 표준 라이브러리에서

각 연산자 토큰이 어떤 트레잇 메서드로 매핑되는지에 대한 정확한 표는 표준 라이브러리에서 정의합니다. 본 문서는 매핑이 존재한다는 사실과 디스패치 메커니즘에 대한 부분만을 책임지고 있습니다.

표준 라이브러리는 메타 시스템의 #[infix] / #[prefix] / #[postfix] 속성을 통해 매핑을 트레잇 메서드에 부착합니다. 정확한 메커니즘은 #[infix] / #[prefix] / #[postfix] 매직 에서 정의하고 있으니 참고합니다.

trait Add<Rhs = Self> {
    type Output;
    #[infix("+")]
    fn add(self, rhs: Rhs) -> Output;
}

trait Neg {
    type Output;
    #[prefix("-")]
    fn neg(self) -> Output;
}

위 예시는 트레잇과 메서드 시그니처의 모양을 보여주고 있으며, 2장의 트레잇 사양과 6장의 매직 사양이 합쳐져서 의미를 만들어 내게 됩니다. 본 3장에서는 언어가 토큰을 보고 위와 같은 호출로 변환한다는 사실까지를 정의합니다.

3.1.3. 디스패치는 vtable 없이

연산자 호출의 디스패치는 트레잇-as-타입 금지 의 원칙에 따라 vtable 간접 호출 없이 해소되도록 설계됩니다.

  • 정적 슬롯의 경우, 컴파일 타임에 트레잇 메서드가 결정되어 직접 함수 호출로 컴파일됩니다.
  • any 슬롯의 경우, any 슬롯 의 런타임 태그 검사를 통해 트레잇 메서드를 결정한 뒤 직접 호출합니다. 비용은 태그 검사 한 번에 직접 호출이 더해지는 정도입니다.

두 경우 모두 vtable lookup 으로 인한 추가 간접 호출이 발생하지 않습니다. 과학·수치 코드의 핵심 루프가 이러한 비용을 치르지 않도록 하는 것이 트레잇-as-타입 금지 의 동기이며, 그 귀결이 바로 본 디스패치 규칙입니다.

impl 고르기

이항 연산 a op b 는 왼쪽 피연산자 타입의 그 연산자 트레잇 impl 에서 고릅니다.

  • 한 타입에 같은 연산자 트레잇의 impl 이 여럿이면 (Mul<i64>, Mul<f64> 등), Rhs 가 오른쪽 피연산자의 타입과 맞는 impl 을 고릅니다.
  • 왼쪽이 원시 타입이고 오른쪽이 struct 이면, 원시 타입에 대해 선언된 impl 을 씁니다 (2.0 * d 는 impl Mul<Duration> for f64).

같은 규칙은 연산자에 한하지 않습니다. 트레잇의 타입 매개변수를 채우는 값의 타입으로 impl 을 고르는 모든 자리에 적용됩니다. 연산자의 Rhs, T::from(x) / T::try_from(x) / Tr::f(x) 의 인자, 타입이 정해진 자리의 .into() / .try_into() 수신자, 그리고 impl 들이 그 매개변수에서 다를 때 메서드 호출 x.m(v) 나 경로 호출 T::m(x, v) 의 인자가 그렇습니다. 그 값이 접미사 없는 리터럴이면 다음 순서로 고릅니다.

  1. 리터럴의 기본 타입 (i32 / f64) 의 impl 이 있고 값이 그 타입에 들어가면 그것.
  2. 아니면 매개변수 타입이 그 값을 담을 수 있는 유일한 impl (정수는 범위 안의 정수 타입 또는 int, 실수는 유한하게 표현되는 f32 / f64 또는 float). d * 2 는 Duration 의 Mul<i64> 입니다.
  3. 그런 impl 이 여럿이면 접미사를 요구하는 컴파일 에러.

이 규칙은 맨 리터럴과 부호를 붙인 리터럴 (-1) 에만 적용되며, 1 + 1 같은 식에는 적용되지 않습니다. Tr::f(..) 처럼 부른 정적 트레잇 메서드의 Self 는 기대 타입에서 정합니다. 트레잇의 매개변수를 채우는 인자가 없으면, 호출의 기대 타입이 그것을 반환 타입으로 갖는 impl (또는 bound) 을 고르며, 그렇지 않으면 호출은 모호합니다 (4.9.6).

3.1.4. 매핑이 없는 연산

특정 타입 쌍에 대해 트레잇 구현이 없는 경우, 그 연산자 호출은 컴파일 에러로 처리됩니다.

let a: i32 = 1i32;
let b: str = "hi";
let c = a + b;          // ERROR: Add 의 (i32, str) impl 없음

이때 진단은 어떤 트레잇이 빠져 있는지를 명시합니다.

ERROR: cannot apply '+' to 'i32' and 'str'
       (no impl Add<str> for i32)

3.1.5. 단항, 이항, 후위

연산자는 그 위치에 따라 다음 셋 중 하나로 분류되고 있습니다.

위치형태매핑 메커니즘
이항 (infix)a OP b#[infix("OP")]
전위 단항 (prefix)OP a#[prefix("OP")]
후위 단항 (postfix)a OP#[postfix("OP")]

같은 토큰이 여러 위치에 등장하는 경우도 있습니다. 예를 들어 - 은 이항 (a - b) 과 전위 단항 (-a) 양쪽 모두에 쓰이게 됩니다. 위치는 토큰이 등장한 문맥에 따라 결정되는데, 피연산자가 앞에 있으면 후위로, 양쪽에 있으면 이항으로 해석되는 식입니다.

3.1.6. 우선순위와 결합

위치별 디스패치 외에도, 연산자는 우선순위 (precedence) 와 결합성 (associativity) 을 갖고 있습니다. 이들에 대한 내용은 우선순위와 결합성 에서 정의합니다.

우선순위와 결합성은 토큰 → 트레잇 매핑의 일부가 아닙니다. 3.2 표가 언어 차원에서 고정하며, #[infix("+")] 같은 등록은 토큰과 트레잇 메서드를 연결하기만 합니다.

3.1.7. 사용자 정의 연산자

표준 라이브러리는 #[infix] / #[prefix] / #[postfix] 를 통해 기본 연산자에 의미를 부여합니다. 사용자 코드 역시 같은 메커니즘을 사용하여 추가 트레잇에 새로운 매핑을 부착할 수 있습니다.

#[infix("⊕", like = "+")]          // 새 토큰은 기존 토큰의 단계를 따름
fn combine(self, rhs: Rhs) -> Output;

다만 같은 (토큰, 위치, 타입 쌍) 에 대한 매핑은 유일해야 하며, 충돌이 발생할 경우 컴파일 에러로 처리됩니다.

구두점과 연산자 토큰 의 고정 토큰표에 없는 글자열을 새로운 연산자로 등록하시려면 같은 메커니즘을 통해 토큰 자체를 등록해야 합니다. 정확한 절차는 #[infix] / #[prefix] / #[postfix] 매직 에서 정의합니다.

3.1.8. Unicode 연산자

≤, ≥, ≠, ⊕, ∈ 등 ASCII 영역 밖의 연산자에 대해서는, 표준 라이브러리가 같은 트레잇의 다중 alias 로 등록할 수 있도록 하고 있습니다.

trait Cmp<Rhs = Self> {
    #[infix("<=")]
    #[infix("≤")]
    fn le(self, rhs: Rhs) -> bool;
}

이처럼 같은 트레잇 메서드에 여러 토큰이 매핑되어 동일한 동작을 하게 됩니다. 따라서 사용자께서는 둘 중 어느 표기를 쓰시더라도 같은 결과를 얻으실 수 있습니다.

GitHub에서 이 페이지 고치기