Bindings
A binding gives a name to a value. Rux has three kinds, each introduced by its own keyword:
| Keyword | Binds | Can change | Computed | Page |
|---|---|---|---|---|
let | an immutable run-time value | no | at run time | this page |
var | a mutable run-time value | yes | at run time | this page |
const | a compile-time value | no | by the compiler | Constants |
binding = ("let" | "var") pattern [":" type] [initializer] ";"
initializer = "=" expression
| "<-" expression
The pattern is usually a single name. It can also take a tuple or a structure apart — see Destructuring.
let answer = 42;
var counter = 0;
counter += 1;
let
let binds a value that never changes. The name keeps the value it was given for as long as it is in scope:
let limit: int32 = 10;
limit = 11;
error: cannot modify immutable variable 'limit'
help: declare 'limit' with 'var' to make it mutable
Because a let can never be assigned later, it must be initialized where it is declared. let total: int32; is error: immutable variable requires an initializer.
Prefer let. A name that cannot change is one less thing for the reader to track, and the compiler rejects an accidental write instead of letting it through.
var
var binds a value that may be replaced. Assignment, the compound operators and ++/-- all need a var (or another mutable place, such as a field of one):
var score: int32 = 10;
score = 12;
score += 5;
score++;
A var may also be declared without a value and assigned later. Until then it holds nothing, and the compiler makes sure no path reads it too early — see Initialization.
Types and inference
With an initializer and no annotation, the binding takes the initializer's type. Literals have a default type:
| Initializer | Inferred type |
|---|---|
42, 3000000000 | int |
1.5 | float64 |
'A' | char32 |
true | bool |
"text" | char8[..] |
7u8, 2.5f32, c8'A' | the suffix or prefix's type |
An annotation, : Type, chooses the type instead. An unsuffixed integer literal then takes the annotated type, provided its value fits:
let small: uint8 = 200;
let wide: int64 = 5;
let small: uint8 = 300; is error: integer literal is out of range for type 'uint8'.
A binding's type is fixed once it is declared. A value of another type is not converted to fit, with one exception: an integer widens to a wider integer of the same signedness, because no value can change on the way. Every other change of type is written with as:
let narrow: int32 = 7;
let widened: int64 = narrow;
let converted = narrow as float64;
The reverse, let back: int32 = widened;, is error: cannot assign 'int64' to 'int32'.
= and <-
An initializer is written with = or <-, and the two mean different things:
| Form | Meaning |
|---|---|
let b = a; | copy: b gets its own copy, and a is unchanged |
let b <- a; | move: the value is handed to b, and a may not be read again |
let first = Buffer { size: 4 };
let second <- first;
PrintLine("{}", second.size);
Reading first after the move is error: value 'first' is used after it was moved. A type that cannot be copied, such as one that owns a resource, can only be bound with <- from a named source; a freshly made value, such as the result of a call, needs no <-. The full rules are in Copy and move.
Mutability
Mutability belongs to the binding and reaches all the way into the value. A let structure cannot have any field changed, at any depth; a var one can have every field changed, or be replaced whole:
let fixed = Point { x: 1, y: 2 };
var moving = Point { x: 1, y: 2 };
moving.x = 5;
moving = Point { x: 0, y: 0 };
fixed.x = 5; is error: cannot modify immutable variable 'fixed'. There is no per-field opt-out in either direction. The same holds for tuples and fixed-size arrays.
A function changes a caller's value only through a writable reference, &var T, or a writable pointer, *var T — see References and Pointers. A parameter itself is always immutable; a function that needs a mutable copy moves the argument into a local, as in var local <- value;.
Scope
A binding is visible from its declaration to the end of the block that contains it. Declaring the same name twice in one block is an error:
let total = 1;
let total = 2;
error: variable 'total' is already declared in this scope
Each name a destructuring pattern declares counts separately, so let (p, p) = (1, 2); is refused the same way.
A block nested inside another — the body of an if or a loop — may declare a name that is already used outside it. The inner binding hides the outer one until the block ends.
rux 0.4.0 does not yet keep a hiding binding apart from the one it hides: after the inner block, the outer name can read the inner value. Until that is fixed, give a binding in a nested block a name of its own.
Discarding a value
_ is not a name. let _ = value; evaluates value and throws it away, and nothing can read _ afterwards — see Destructuring.
See also
- Constants — names the compiler computes
- Initialization — declaring a
varnow and giving it a value later - Destructuring — binding several names from one value
- Assignment —
=,<-, compound assignment,++and-- - Copy and move — what
=and<-do to the source - Learn: Variable, Mutable
Aliases
Declaring a second name for a type with type Name = T, what an alias does and does not change, and the built-in aliases bool, byte, char and float.
Constants
const declarations: values the compiler computes once, what their initializer may contain, where they may appear, and their use as array sizes.