2.4. 복합 대입 트레잇
rok::core::ops 의 *Assign 트레잇 패밀리는 a += b, a *= b 같은 복합 대입 연산자를 in-place 갱신으로 dispatch 하는 역할을 담당합니다. 즉, refs-lang 3.4 대입과 복합 대입 에서 정의된 syntax 가 본 트레잇으로 desugar 되는 구조입니다.
참고
rok 은 mut 키워드를 일반 코드에서 쓰지 않습니다 (refs-lang 5.1 변수 선언).
변수는 항상 let 으로 선언되고, 같은 이름에 다시 대입하면 재할당 으로 처리됩니다.
heap 객체 (List, Dict 등) 의 in-place 갱신은 변수 선언자 let mut 가 아니라
객체가 가변 메서드를 노출하는지 로 결정됩니다. *Assign 트레잇이 그 가변 메서드의 자리입니다.
2.4.1. 핵심 차이 - += vs = then +
a += b;
이 식에는 두 가지 의미가 가능합니다. 먼저 in-place dispatch 의 경우 <typeof(a) as AddAssign<typeof(b)>>::add_assign(ref a, b) 로 풀려서 a 가 가리키는 객체의 내용 을 직접 수정하므로 새 객체를 만들지 않습니다. 반면 fallback (Add::add 후 재대입) 의 경우 a = a + b 형태가 되어 Add 호출 결과를 a 에 다시 저장하면서 새 객체를 생성합니다.
rok 은 이 두 모드를 자동으로 선택합니다.
컴파일러 동작 (a += b 받았을 때):
1. typeof(a) 가 AddAssign<typeof(b)> 구현했으면
→ AddAssign::add_assign(ref a, b) dispatch
2. 그렇지 않고 typeof(a) 가 Add<typeof(b)> 구현했으면
→ a = Add::add(a, b) 로 desugar
3. 둘 다 없으면 컴파일 에러
이러한 정책 덕분에 컬렉션의 in-place 최적화가 가능해집니다. 예를 들어 xs: List<i32> 가 xs += y 를 수행할 때 새 List 를 만들지 않고 직접 push 됩니다.
2.4.2. 산술 *Assign 트레잇
AddAssign - +=
trait AddAssign<Rhs = Self> {
#[infix("+=")]
fn add_assign(ref self, rhs: Rhs);
}
let counter: i32 = 0;
counter += 5; //→ 5 (counter 가 in-place 갱신)
counter += 3; //→ 8
let xs: List<i32> = [1, 2];
xs += [3, 4]; //→ [1, 2, 3, 4] (List 의 add_assign - push 효율)
SubAssign - -=
trait SubAssign<Rhs = Self> {
#[infix("-=")]
fn sub_assign(ref self, rhs: Rhs);
}
MulAssign - *=
trait MulAssign<Rhs = Self> {
#[infix("*=")]
fn mul_assign(ref self, rhs: Rhs);
}
DivAssign - /=
trait DivAssign<Rhs = Self> {
#[infix("/=")]
fn div_assign(ref self, rhs: Rhs);
}
RemAssign - %=
trait RemAssign<Rhs = Self> {
#[infix("%=")]
fn rem_assign(ref self, rhs: Rhs);
}
ModuloAssign - %%=
trait ModuloAssign<Rhs = Self> {
#[infix("%%=")]
fn modulo_assign(ref self, rhs: Rhs);
}
%%= 는 Modulo (floored modulo, 결과 부호 = 제수 부호) 의 복합 대입 형태입니다. RemAssign 과 다른 트레잇으로 분리되어 있으며, 양수 제수에 대해 결과가 항상 [0, rhs) 범위에 있도록 in-place 갱신합니다.
2.4.3. 비트 *Assign 트레잇
BitAndAssign - &=
trait BitAndAssign<Rhs = Self> {
#[infix("&=")]
fn bitand_assign(ref self, rhs: Rhs);
}
let flags: u32 = 0xFF;
flags &= 0x0F; //→ 0x0F
BitOrAssign - |=
trait BitOrAssign<Rhs = Self> {
#[infix("|=")]
fn bitor_assign(ref self, rhs: Rhs);
}
BitXorAssign - ^=
trait BitXorAssign<Rhs = Self> {
#[infix("^=")]
fn bitxor_assign(ref self, rhs: Rhs);
}
ShlAssign - <<=
trait ShlAssign<Rhs = Self> {
#[infix("<<=")]
fn shl_assign(ref self, rhs: Rhs);
}
let n: i32 = 1;
n <<= 4; //→ 16
ShrAssign - >>=
trait ShrAssign<Rhs = Self> {
#[infix(">>=")]
fn shr_assign(ref self, rhs: Rhs);
}
2.4.4. 시그니처 패턴 - ref self
모든 *Assign 메서드는 ref self 를 받습니다 (refs-lang 4.7 ref keyword). 이렇게 함으로써 caller 의 변수 binding 자체를 참조해 in-place 수정이 이루어집니다.
fn add_assign(ref self, rhs: Rhs);
^^^^^
caller 의 변수가 가리키는 객체를 직접 수정
이 시그니처가 in-place 의 핵심에 해당합니다. 만일 self (값) 만 받게 된다면 복사본을 수정하는 데 그쳐서 caller 측에서는 변경 결과를 볼 수 없게 됩니다.
2.4.5. 우선순위 / 결합성 - prec=-1, right
모든 *Assign 토큰은 refs-lang 3.2 표에서 동일한 등급으로 고정됩니다. 우선순위 측면에서 prec = -1 을 가지므로 일반 산술 (+=7, *=8) 보다 낮고, .. (=0) 보다도 낮은 위치에 자리합니다. 가장 낮은 등급입니다. 연쇄 대입은 허용하지 않습니다 (refs-lang 3.4.4).
2.4.6. 트레잇 구현 매트릭스
primitive 타입의 *Assign impl 에는 모두 #[intrinsic("rok.OP_assign.TYPE")] 가 부착됩니다.
| 타입 | 트레잇 | 동작 |
|---|---|---|
i*, u* | 산술 6 개 + 비트 5 개 (11 트레잇) | rok.add_assign.i32, rok.shl_assign.u32, ... |
f32, f64 | 산술 6 개만 | rok.add_assign.f64, ... |
bool | BitAndAssign/BitOrAssign/BitXorAssign | rok.and_assign.bool, ... |
int | 산술 + 비트 | auto-promote 로직 - 새 값으로 교체 (불변) |
float | 산술 | (위와 동일 - auto-promote) |
List<T> | AddAssign<List<T>>, AddAssign<T> | push |
str | AddAssign<str>, AddAssign<char> | s = s + t - 새 str 로 교체 (str 은 불변) |
Dict<K,V> | AddAssign<Dict<K,V>> | merge - 기존 키 덮어씀 |
a op= b 는 왼쪽 타입이 XxxAssign 을 구현하면 XxxAssign::xxx_assign (AddAssign::add_assign 등) 을 부르고, 아니면 a = a op b 입니다.
2.4.7. 컬렉션의 *Assign - 핵심 사용처
List<T> / Dict<K,V> 같은 가변 컨테이너에서는 *Assign 가 진짜 in-place 동작을 수행합니다.
let xs: List<i32> = [1, 2, 3];
let alias = xs; // 같은 객체 공유 (Python 식, refs-lang 4.7.x)
xs += [4, 5]; // xs 에 push (새 객체 안 만듦)
alias; //→ [1, 2, 3, 4, 5] (alias 도 변화 봄)
// 비교: xs = xs + [4, 5] 는 새 List 생성, alias 는 변화 못 봄
이 의미는 rok 의 heap 객체 자동 공유 모델 (refs-lang 4.7) 과 정확히 일치합니다.
2.4.8. 사용자 정의 *Assign
사용자 타입이 Add 만 구현하고 AddAssign 을 구현하지 않은 경우 컴파일러가 fallback 을 사용합니다.
struct Vec3 { x: f64; y: f64; z: f64; }
impl Add for Vec3 {
type Output = Vec3;
fn add(self, rhs: Vec3) -> Vec3 { ... }
}
// AddAssign 정의 안 됨
let v = Vec3 { x: 1.0, y: 2.0, z: 3.0 };
v += Vec3 { x: 0.5, y: 0.5, z: 0.5 };
// → 컴파일러: AddAssign 없음, Add 있음 → desugar 'v = v + (...)'
// Vec3 가 immutable 이라 새 객체 생성 후 v 재대입
성능이 중요한 경우라면 사용자가 AddAssign 을 명시적으로 구현하는 편이 권장됩니다.
impl AddAssign for Vec3 {
fn add_assign(ref self, rhs: Vec3) {
self.x += rhs.x;
self.y += rhs.y;
self.z += rhs.z;
}
}
2.4.9. *Assign 의 supertrait 관계 - 없음
rok 은 AddAssign 을 Add 의 supertrait 로 묶지 않습니다. 따라서 두 트레잇은 서로 독립적으로 다루어집니다.
그 이유는 컬렉션처럼 Add 가 의미가 없고 (xs + ys 가 새 컬렉션을 만드는 비용이 큼) AddAssign 만 의미가 있는 경우가 존재하기 때문입니다. 만약 supertrait 로 묶는다면 그러한 타입이 AddAssign 만 구현하는 일이 불가능해집니다.
2.4.10. 관련 - broadcast *Assign?
2.6 broadcast 의 19 개 토큰 (.+, .*, .%%, .&&, ...) 에 대응하는 복합 대입 변형은 정의되지 않습니다. 즉 a .+= b 같은 토큰은 존재하지 않습니다. broadcast 의 결과는 보통 새 컨테이너를 생성하기 때문에 in-place 의미가 모호해진다는 점이 그 배경입니다.
필요한 경우 사용자가 dot_add_assign 같은 메서드를 명시적으로 호출하면 됩니다.
xs.dot_add_assign(ys); // 메서드 호출 (broadcast in-place)
impl<T> List<T> {
fn dot_add_assign(self, rhs: List<T>) where T: Add<Output = T>;
// dot_OP_assign 도 같음 - OP 는 add, sub, mul, div, rem, modulo,
// bitand, bitor, bitxor, shl, shr 이며 각각 T: OP<Output = T> 필요
}
길이가 같아야 하며, 다르면 panic 입니다. Slice 형태는 없습니다.