rokdocs
v0.1

2.4. Compound assignment traits

The *Assign trait family of rok::core::ops dispatches compound assignment operators such as a += b and a *= b as in-place updates. The syntax defined in refs-lang 3.4 Assignment and compound assignment desugars to these traits.

Note

rok does not use a mut keyword in ordinary code (refs-lang 5.1 Variable declarations). Variables are always declared with let, and assigning to the same name again is a reassignment. In-place update of a heap object (List, Dict, etc.) is determined not by a let mut declarator but by whether the object exposes mutating methods. The *Assign traits are where those mutating methods live.

2.4.1. The key difference - += vs = then +

a += b;

This expression has two possible meanings. With in-place dispatch, it resolves to <typeof(a) as AddAssign<typeof(b)>>::add_assign(ref a, b), which directly modifies the contents of the object a refers to and creates no new object. With the fallback (Add::add followed by reassignment), it becomes a = a + b, storing the result of the Add call back into a and creating a new object.

rok chooses between the two modes automatically.

Compiler behavior (on a += b):
1. If typeof(a) implements AddAssign<typeof(b)>
     → dispatch AddAssign::add_assign(ref a, b)
2. Otherwise, if typeof(a) implements Add<typeof(b)>
     → desugar to a = Add::add(a, b)
3. If neither, compile error

This policy enables in-place optimization of collections. For example, when xs: List<i32> performs xs += y, it pushes directly without creating a new List.

2.4.2. Arithmetic *Assign traits

AddAssign - +=

trait AddAssign<Rhs = Self> {
    #[infix("+=")]
    fn add_assign(ref self, rhs: Rhs);
}
let counter: i32 = 0;
counter += 5;       //→ 5 (counter is updated in place)
counter += 3;       //→ 8

let xs: List<i32> = [1, 2];
xs += [3, 4];       //→ [1, 2, 3, 4]  (List's add_assign - efficient 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);
}

%%= is the compound assignment form of Modulo (floored modulo, sign of result = sign of divisor). It is a separate trait from RemAssign, and updates in place so that, for a positive divisor, the result always lies in [0, rhs).

2.4.3. Bitwise *Assign traits

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. Signature pattern - ref self

Every *Assign method takes ref self (refs-lang 4.7 ref keyword). It refers to the caller's variable binding itself, so the modification happens in place.

fn add_assign(ref self, rhs: Rhs);
       ^^^^^
       directly modifies the object the caller's variable refers to

This signature is the core of in-place behavior. If it took only self (by value), it would modify a copy, and the caller would not see the change.

2.4.5. Precedence / associativity - prec=-1, right

Every *Assign token is fixed at the same level in the refs-lang 3.2 table. Its precedence is prec = -1, lower than ordinary arithmetic (+=7, *=8) and lower than .. (=0). It is the lowest level. Chained assignment is not allowed (refs-lang 3.4.4).

2.4.6. Trait implementation matrix

Every *Assign impl for primitive types carries #[intrinsic("rok.OP_assign.TYPE")].

TypeTraitsBehavior
i*, u*6 arithmetic + 5 bitwise (11 traits)rok.add_assign.i32, rok.shl_assign.u32, ...
f32, f646 arithmetic onlyrok.add_assign.f64, ...
boolBitAndAssign/BitOrAssign/BitXorAssignrok.and_assign.bool, ...
intArithmetic + bitwiseauto-promote logic - replaced by a new value (immutable)
floatArithmetic(same as above - auto-promote)
List<T>AddAssign<List<T>>, AddAssign<T>push
strAddAssign<str>, AddAssign<char>s = s + t - replaced by a new str (str is immutable)
Dict<K,V>AddAssign<Dict<K,V>>merge - overwrites existing keys

a op= b calls XxxAssign::xxx_assign (AddAssign::add_assign, ...) when the left type implements it; otherwise it is a = a op b.

2.4.7. *Assign on collections - the main use

For mutable containers such as List<T> / Dict<K,V>, *Assign performs a true in-place operation.

let xs: List<i32> = [1, 2, 3];
let alias = xs;             // shares the same object (Python-style, refs-lang 4.7.x)

xs += [4, 5];               // push onto xs (no new object)
alias;                       //→ [1, 2, 3, 4, 5]  (alias sees the change too)

// compare: xs = xs + [4, 5] creates a new List, and alias does not see the change

This meaning matches rok's automatic sharing model for heap objects (refs-lang 4.7) exactly.

2.4.8. User-defined *Assign

If a user type implements only Add and not AddAssign, the compiler uses the fallback.

struct Vec3 { x: f64; y: f64; z: f64; }

impl Add for Vec3 {
    type Output = Vec3;
    fn add(self, rhs: Vec3) -> Vec3 { ... }
}
// AddAssign not defined

let v = Vec3 { x: 1.0, y: 2.0, z: 3.0 };
v += Vec3 { x: 0.5, y: 0.5, z: 0.5 };
// → compiler: no AddAssign, Add exists → desugar 'v = v + (...)'
// Vec3 is immutable, so a new object is created and reassigned to v

Where performance matters, the user is advised to implement AddAssign explicitly.

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. Supertrait relationship of *Assign - none

rok does not make Add a supertrait of AddAssign. The two traits are independent.

The reason is that some types, such as collections, have no meaningful Add (xs + ys is expensive because it creates a new collection) but do have a meaningful AddAssign. A supertrait relationship would make it impossible for such types to implement only AddAssign.

No compound assignment variants are defined for the 19 tokens of 2.6 broadcast (.+, .*, .%%, .&&, ...). That is, a token such as a .+= b does not exist. The background is that broadcast results usually create a new container, which makes in-place meaning ambiguous.

Where needed, the user calls a method such as dot_add_assign explicitly.

xs.dot_add_assign(ys);    // method call (broadcast in place)
impl<T> List<T> {
    fn dot_add_assign(self, rhs: List<T>) where T: Add<Output = T>;
    // likewise dot_OP_assign for OP in add, sub, mul, div, rem, modulo,
    // bitand, bitor, bitxor, shl, shr - each requiring T: OP<Output = T>
}

The lengths must match; otherwise it panics. There is no Slice form.

Edit this page on GitHub