Bindings
A const binding never changes and nothing below it changes; a var binding can be changed in place. That one rule replaces every mutable-and-immutable type pair.
A binding gives a value a name. const means the name and everything reachable through it never changes; var means it
can be changed through this name. There is no third form, and no type in the language has a mutable twin.
Example#
const answer = 42
var counter = 0
counter = counter + 1
var list = [1, 2]
list.append 3
const fixed = list
print "{answer} {counter} {list} {fixed}"Syntax#
const <pattern> [: <Type>] = <expression>
var <pattern> [: <Type>] = <expression>A pattern destructures here too, so const (quotient, remainder) = divide(7, 2) and const Point(x, y) = p are
bindings that split their value into several names at once. See
Cases and match for the pattern vocabulary a binding shares with match.
A module's own top-level const is the one exception: it binds a single name, because a name another file imports
has to be one thing (see Top-level code).
Rules#
A binding is always initialized.
var xandvar x: Intare both compile errors. No type in the language has an implied default value, so there is nothing a bare declaration could mean.Does not compilevar later: Int // error: A binding needs a valueconstis deep from the perspective of the binding. Through aconstbinding you can neither reassign, nor assign a field, nor call a method that is avar fn.varmeans "changeable through this path".Does not compileconst fixed = [1, 2] fixed.append 3 // error: `append` needs a `var`The binding decides about the value, not the type. There is no
ListandMutableList, noPointandMutablePoint, and no read-only view of anything. A parameter withoutvaris the immutable form.Assigning, passing and capturing is a copy. Two bindings never refer to the same value, so a change through one is invisible through the other. The exception is a
shared type, which has an identity.type Point { var x: Int var y: Int } var first = Point 1, 2 const second = first first.x = 99 print "{first} {second}" // prints Point(x: 99, y: 2) Point(x: 1, y: 2)That prints
Point(x: 99, y: 2) Point(x: 1, y: 2).A type annotation is optional, and a literal adapts to it. Without one, an integer literal is
Int64and a decimal literal isFloat64. With one, the literal takes the expected type, and a literal that does not fit it is a compile error at the literal.const ratio: Float = 1 const byte: UInt8 = 0xFF const million = 1_000_000Does not compileconst small: Int8 = 300 // error: `300` does not fit into `Int8`An empty collection literal needs a type from context.
[]and[:]carry no element type of their own.const empty: List<String> = [] const table: Map<String, Int> = [:] print "{empty} {table}"A name can be shadowed in a nested scope and never redeclared in the same one. The parameters of a function and the top level of its body are one scope, so a
constthere may not take a parameter's name. A nested block and a closure are scopes of their own. There is no silent shadowing anywhere in the language.A change that cannot have an effect is a compile error. A
varthat is changed and never read afterwards, and the discarded result of a method that only reads its receiver, are both errors: with value semantics they are always mistakes rather than defensive lines. Discard on purpose withconst _ = ....An expression statement has the type
VoidorNever, unless the call has avarreceiver or avarargument. That is the rule behind rule 8:parser.bump()changes something and stays a statement, whileEmail.tryFrom(text)and1 + 2are values that go nowhere.Assignment is a statement, not an expression.
a = b = cdoes not parse, andif x = yis not a comparison that was mistyped - it does not parse either.
What this is not#
const is not final, and it is not readonly. In Java, C# and TypeScript those words freeze the binding and
leave the object writable. Here the value is frozen too:
var writable = [1, 2]
writable.append 3
print writableconst frozen = [1, 2]
frozen.append 3
// error: `append` needs a `var`var is not let mut, because there is nothing to borrow. A var binding owns its value. Handing it to a function
that may change it is a var parameter, and that reference exists only for the duration of the call.
Taking an element out of a collection is not a reference to it. This is the copy trap. The first program below
prints 1, the second prints 1 0: the copy changed and the list did not. Rule 8 makes a change that is never read a
compile error. The second program reads the copy in its last line, so there is nothing to report: a copy that is
changed and then read is a legal program that does something else than was meant.
type Counter {
var count: Int = 0
var fn increment() {
count = count + 1
}
}
var counters = [Counter(), Counter()]
counters[0].increment()
print counters[0].count
// prints 1type Counter {
var count: Int = 0
var fn increment() {
count = count + 1
}
}
var counters = [Counter(), Counter()]
var first = counters[0]
first.increment()
print "{first.count} {counters[0].count}"A const binding is not a compile-time constant. Its initializer runs at runtime like any other expression. The one
place where a constant has to be evaluable at compile time is a top-level const of a module, which is a different rule.
Related#
- Declaring a type - fields,
varfields and what is generated. - Mutation and var paths - what has to be
varfrom the binding down. - Values and bindings - the same material as a learning step.
- Why values instead of references - the argument behind rule 4.
- Coming from Rust - if
let mutand&mutare what you reach for.