Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Augmented assignment

Andy C++ supports augmented assignment for operations on existing variables.

This example increments a number with augmented assignment.

let my_number = 3;
my_number += 5;

assert_eq(my_number, 8);

Indexed targets

The target of an augmented assignment can also be an indexed location:

let values = [1, 2, 3];
values[0] += 10;
assert_eq(values, [11, 2, 3]);

This works even when reading the location produces a new value rather than a reference, such as a character of a string or a slice of a list. The updated value is always stored back into the container:

let text = "ab";
text[0] ++= "x";
assert_eq(text, "axb");

let items = [1, 2];
items[0..1] ++= [3];
assert_eq(items, [1, 3, 2]);

The target, index, and right-hand side are each evaluated exactly once, in source order. The augmented assignment expression itself evaluates to ().

Type checking

An augmented assignment with an in-place operator such as ++= never changes the type of its target. If the right-hand side would force a type change the program is rejected:

let values = [1];
values ++= ["two"]; // error: mismatched types: found List<String> but expected List<Int>

The right-hand side has to provably fit. A wider element type is not enough, because the operator copies the values across without checking them:

let values: List<Int> = [1];
let rhs: List<Number> = [0.5];
values ++= rhs; // error: mismatched types: found List<Number> but expected List<Int>

The same goes for an operand whose type is unknown. Any says nothing about what the value holds, and no later step re-checks it, so it is rejected too:

fn opaque(x) => x;
let values = [1];
values ++= opaque([2, 3]); // error: mismatched types: found Any but expected List<Int>

Cast the operand to say what it holds. The cast checks the value, so the assignment gets its guarantee from the cast site:

fn opaque(x) => x;
let values = [1];
values ++= opaque([2, 3]) as List<Int>;
assert_eq(values, [1, 2, 3]);

A cast that does not hold fails there rather than corrupting the target:

values ++= opaque([0.5]) as List<Int>; // error: cannot cast List<Float> to List<Int>

Annotate the target with Any to opt into heterogeneous contents:

let mixed: List<Any> = [1];
mixed ++= ["two"];
assert_eq(mixed, [1, "two"]);

Operators without an in-place implementation behave like target = target op value and may widen the inferred type of the target, just like a regular assignment:

let numbers = [1];
numbers[0] += 0.5; // fine: the element type widens from Int to Any
assert_eq(numbers, [1.5]);

Optimization

You might expect list ++= [1,2,3] to desugar to list = list ++ [1,2,3], but that would waste work. Andy C++ handles some augmented assignments directly. In this case, it appends [1,2,3] without creating an intermediate list.

Flexibility

Note: I stole this feature from Noulith.

Augmented assignment also works with built-in functions and user-defined functions. For example:

let x = 3;
let f = fn (a, b) { a + b }; // simple addition
x f= 5; // similar to: x = f(x, 5);
assert_eq(x, 8);

One common use case is tracking the highest or lowest value in a loop:

let lowest, highest = Inf, -Inf;

for x in 1..100 {
  lowest min= g(x);
  highest max= g(x);
}