3.4. Assignment and compound assignment
Assignment (=) and compound assignment (+=, -=, etc.) are expressions that update the value of a variable, a field, or an indexed place. The grammar is:
assign_expr ::= lvalue assign_op expr
assign_op ::= "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|="
| "^=" | "<<=" | ">>="
lvalue ::= identifier
| lvalue "." identifier
| lvalue "." int_lit // tuple position
| lvalue "[" expr "]" // index
| lvalue "[" range_expr "]" // slice
3.4.1. Simple assignment
Simple assignment has the following shape.
x = expr;
obj.field = expr;
arr[i] = expr;
The left-hand side (lvalue) is a reassignable place, the right-hand side (rvalue) is any expression, and the result value has the unit type (()). Assignment itself is an expression, but because its result is unit it is normally used as a statement. As a result, patterns like C's if (x = read()), which are rarely needed, are blocked by the unit result.
Kinds of lvalue
The forms allowed on the left-hand side and their meanings:
| Form | Example | Meaning |
|---|---|---|
| simple variable | x = ... | writes the value to the variable's slot |
| field access | obj.field = ... | writes the value to the object's field |
| tuple position | pair.0 = ... | writes the value to a position of a tuple struct / tuple |
| indexing | arr[i] = ... | writes the value to the indexed place |
| slicing | arr[1:3] = ... | writes the value to the slice region |
Indexing and slicing assignments desugar to the write traits of indexing and slicing.
Places that cannot be lvalues
The following cannot be lvalues.
1 = 2; // a literal is not an lvalue
foo() = 2; // a call result is generally not an lvalue
(x + 1) = 2; // an arithmetic result is not an lvalue
Whether something can be an lvalue is checked at compile time and determined by form (shape) alone. No runtime check is performed.
3.4.2. Declaring a new variable vs. reassignment
Combined with the let keyword, two forms are distinguished.
let x = 1; // new declaration, 'let' required
x = 2; // reassignment, without 'let'
Creating a new variable requires let; writing again to an existing variable's slot is written without let. Assigning without let to an undeclared name is a compile error. The exact rules are defined in variable declaration.
counter = counter + 1; // OK if counter is declared
coutner = ...; // ERROR: 'coutner' not declared. Did you mean 'counter'?
This rule blocks the trap of a typo silently creating a new variable (a common bug in Julia and Python).
3.4.3. Compound assignment
Compound assignment combines an operation and an assignment in one form.
x += 1; // semantically the same as x = x + 1
x -= 1;
x *= 2;
arr[i] += 1;
All compound assignment tokens of punctuation and operator tokens (+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=) follow the same pattern.
Desugaring and lvalue evaluation
Compound assignment desugars as follows. If the type implements the OpAssign trait, a += b calls it; otherwise it is a = a + b (stdlib 2.4.1). The same holds for every operator.
lvalue OP= rhs → OpAssign::op_assign(ref lvalue, rhs) // if implemented
→ lvalue = (lvalue OP rhs) // otherwise
The compiler guarantees that the lvalue is not evaluated twice.
arr[expensive_index()] += 1;
// expensive_index() is called only once
If the lvalue is a plain variable or field, it is referenced twice as is; if it contains a call or computation, that part is bound to a temporary and evaluated once. This guarantee prevents an index or method with side effects from being called twice unintentionally.
Trait mapping
Each compound assignment maps to a separate Assign trait family.
x += y → <typeof(x) as AddAssign<typeof(y)>>::add_assign(ref x, y)
x -= y → <typeof(x) as SubAssign<typeof(y)>>::sub_assign(ref x, y)
(There are AddAssign, SubAssign, MulAssign, DivAssign, ... and so on.)
These traits may be implemented as in-place updates, allowing a more efficient implementation than x = x + y. For example, appending to a large collection can happen without creating a new collection.
Implementing AddAssign is optional: without it, x += y falls back to x = x + y. x + y itself always needs Add.
Signature
Signatures such as AddAssign have the following shape.
trait AddAssign<Rhs = Self> {
#[infix("+=")]
fn add_assign(ref self, rhs: Rhs);
}
Like other operators, the *Assign traits are registered through #[infix("op=")] (3.1). ref self refers to the lvalue's slot; there is no return type and the result is unit. The exact meaning of ref self and of updating the lvalue slot is defined in the function specification.
3.4.4. Chained assignment
C- and Python-style chained assignment (a = b = c = 1) is not allowed in rok.
a = b = c = 1; // ERROR
Because the result of an assignment is unit, b = c = 1 evaluates to unit and the whole becomes a = (unit), which is meaningless. Write the intended meaning as separate assignments:
c = 1; b = 1; a = 1;
or use destructuring assignment.
3.4.5. Assignment inside an expression
Assignment is an expression with a unit result, so it can be placed in expression position, but there is little value in doing so.
let x = (y = 1); // x = unit (nearly meaningless)
This pattern is discouraged, and tools or linters may warn about it.
Outside statement position, an assignment is written in parentheses (y = 1) or as a match arm body. An unparenthesized let x = y = 1; is not supported.