3.3. 브로드캐스트 연산자
구두점과 연산자 토큰 에서 정의된 점-접두 연산자 (.+, .*, .== 등) 는 원소별 연산 (broadcast) 을 위한 별개의 토큰입니다. 일반 연산자와 동일한 메커니즘, 즉 트레잇 메서드 호출로의 desugar 를 통해 동작하지만, 매핑되는 트레잇은 별도로 분리됩니다.
3.3.1. desugar 패턴
점-접두 연산자는 다음과 같은 형태로 desugar 됩니다.
a .+ b → <typeof(a) as DotAdd<typeof(b)>>::dot_add(a, b)
a .* b → <typeof(a) as DotMul<typeof(b)>>::dot_mul(a, b)
a .== b → <typeof(a) as DotEq<typeof(b)>>::dot_eq(a, b)
이는 연산자 디스패치 에서 다룬 일반 매핑과 같은 형태이며, 대상 트레잇만 다를 뿐입니다. 점-접두 연산자에는 Dot 으로 시작하는 별개의 트레잇 패밀리가 매핑되는데, 그 대응 관계는 다음 표와 같습니다.
| 점-접두 토큰 | 점 없는 형태 | 점-접두 트레잇 |
|---|---|---|
.+ | + ↔ Add | DotAdd |
.- | - ↔ Sub | DotSub |
.* | * ↔ Mul | DotMul |
./ | / ↔ Div | DotDiv |
.% | % ↔ Rem | DotRem |
.%% | %% ↔ Modulo | DotModulo |
.== | == ↔ Eq | DotEq |
.!= | != ↔ Ne | DotNe |
.< | < ↔ (Cmp::lt) | DotLt |
.> | > | DotGt |
.<= | <= | DotLe |
.>= | >= | DotGe |
.&& | && (short-circ) | DotLogicalAnd |
.|| | || (short-circ) | DotLogicalOr |
.& | & | DotBitAnd |
.| | | | DotBitOr |
.^ | ^ | DotBitXor |
.<< | << | DotShl |
.>> | >> | DotShr |
위 트레잇의 정확한 시그니처와 어떤 타입에 어떤 구현이 등록되는지는 표준 라이브러리의 영역에서 정의됩니다. 본 문서는 언어가 점-접두 토큰을 별개의 트레잇 패밀리로 desugar한다는 사실까지를 책임집니다.
중요
점-접두 논리 연산자의 short-circuit 부재: .&&, .|| 는 점 없는 형태와 달리 short-circuit 평가를 하지 않습니다. broadcast 가 fused loop 으로 lower 되는 특성상 양쪽 인자가 모두 평가되며, 의미적으로는 bool 컨테이너에 대한 element-wise 논리 결합과 동일합니다. 부수효과가 있는 함수 호출을 한쪽 인자에 두면 항상 실행되니 주의가 필요합니다.
3.3.2. 우선순위와 결합성
점-접두 연산자의 우선순위와 결합성은 그에 대응하는 점 없는 연산자와 동일하게 적용됩니다.
a .+ b .* c → a .+ (b .* c) // .* 가 .+ 보다 강함 (* > + 와 동일)
a .+ b .+ c → (a .+ b) .+ c // 좌결합
3.3.3. 함수와 메서드 브로드캐스트
함수나 메서드 뒤에 . 을 붙여 모든 원소에 그 함수나 메서드를 적용하는 표기 또한 지원됩니다.
sin.(x) // x 의 각 원소에 sin 적용
f64::sqrt.(arr) // 메서드 형태도 가능
g.(a, b) // 여러 인자
이 형태는 일반 함수 또는 메서드 호출의 결과를 모든 원소에 매핑한 새로운 컬렉션을 반환합니다. desugar 과정에서는 표준 라이브러리의 Iterator::map 호출 (또는 동등한 트레잇) 로 변환됩니다.
f.(x) → x.map(f)
g.(a, b) → a.zip(b).map(|(x, y)| g(x, y))
함수와 메서드의 브로드캐스트는 별개의 트레잇을 두지 않고 반복자 변환의 sugar 로 처리되는데, 이 점이 이항 점-접두 연산자와 다른 부분입니다.
3.3.4. 자동 루프 융합
연속된 점-접두 표현식은 컴파일러가 단일 커널로 융합할 수 있습니다.
sin.(a) .+ cos.(b) .* 2.0f64
위 표현은 가능한 경우 중간 배열을 만들지 않고 한 번의 순회만으로 결과를 만들어냅니다.
이러한 융합은 컴파일 파이프라인 의 MLIR 단계에서 인접한 점-접두 체인을 하나의 linalg 연산으로 인식함으로써 가능해집니다. SIMD 나 GPU 또한 같은 메커니즘을 통해 활용할 수 있습니다.
다만 융합 가능 여부는 컴파일러의 분석에 달려 있으며, 사양 차원에서 항상 보장하지는 않습니다. 대부분의 경우 융합된다는 실용 수준의 약속에 해당합니다.
3.3.5. 일반 연산자와의 의미 차이
중요
점-접두 연산자가 별개의 트레잇으로 분리된 이유는, 같은 자료에 대해 수학적으로 다른 의미를 둘 수 있기 때문입니다.
m * n // 행렬 곱 (Mul 구현, 사용자 정의)
m .* n // 원소별 곱 (Hadamard product, DotMul)
NumPy 의 * 가 암묵적으로 원소별 연산이 되면서 발생했던 혼동을 피하기 위해, rok 은 항상 두 의미를 시각적으로 분리해 둡니다.
이러한 분리는 언어와 표준 라이브러리의 경계 에서 제시된 "구문은 언어, 의미는 라이브러리"라는 원칙을 직접 적용한 결과입니다. 서로 다른 두 의미에 서로 다른 두 구문이 매핑되는 것입니다.
3.3.6. 사용자 정의 트레잇 구현
사용자가 자신의 타입에 점-접두 연산을 부여하고 싶다면, 일반 연산자와 같은 방식으로 Dot 트레잇 패밀리를 구현하면 됩니다.
impl DotAdd for MyVector {
type Output = MyVector;
fn dot_add(self, rhs: MyVector) -> MyVector { ... }
}
별도의 새로운 트레잇이나 매크로 없이도 같은 패턴만으로 점-접두 연산이 지원됩니다.