Mutation and var paths
A change needs an unbroken var path from the binding down to the field being changed, and a var parameter or a var fn receiver is a reference that cannot outlive the call it belongs to.
Nothing in the language is changed through an alias. A change always goes through a var path: a var binding,
var parameter or a var fn receiver, then var fields, indices and ranges all the way down to the value that changes.
Example#
type Counter {
var count: Int = 0
var fn increment() {
count = count + 1
}
}
fn incrementTwice(var target: Counter) {
target.increment()
target.increment()
}
var counter = Counter()
incrementTwice counter
print counter.countSyntax#
<var-path> ::= (<var-binding> | <var-parameter> | "self" in a var-self member) <step>*
<step> ::= "." <var-field> | "[" <index-or-range> "]"Rules#
A
varparameter works on the caller's value, and its argument has to be avarpath.incrementTwicechangescounteritself; there is no marker at the call site, the parameter's ownvaris what promises it.Does not compiletype Counter { var count: Int = 0 var fn increment() { count = count + 1 } } fn incrementTwice(var target: Counter) { target.increment() target.increment() } const frozen = Counter() incrementTwice frozen // error: `incrementTwice` changes `target`, and `frozen` is a `const`A path through
a[key]ora[from..to]changes the collection in place, without a copy.samples[1..4].sort { value => value }sorts that slice ofsamplesitself, the same way a field step would.A reference is second-class: it exists only as a
varparameter or avar fnreceiver, for the duration of one call. It cannot be stored in a field, returned, or captured by a closure that outlives the call - which is exactly what keeps the language free of lifetimes and dangling references. A closure may capture one only where it stands straight as the argument of a call whose parameter only calls it (rule 4), and the checker enforces it.Does not compiletype Counter { var count: Int = 0 var fn watcher(): () => Void { { count = count + 1 } } } // error: This closure captures the receiver `self` and may outlive the callA captured
varbinding follows the same rule as a reference. The closure shares the binding with its scope rather than copying it, so it may only run while the binding exists: it may stand straight as the argument of a call whose parameter only calls it (items.forEach { total = total + _ }, a receiver closure, a block of a DSL), and it may not be bound to a name, stored, returned or handed to a parameter that keeps it. See Closures, rule 8.Does not compilefn counter(): () => Int { var count = 0 { count = count + 1 count } } // error: This closure captures the `var` binding `count` and may outlive itState that has to outlive its scope is a shared type, which says it has an identity; recursion is a local
fn, which captures nothing.The base of a var path can never be a temporary. Changing the result of a call directly is always rejected, because the change would be thrown away the moment the call returns - the value has to be bound to a name first.
Does not compiletype Counter { var count: Int = 0 var fn increment() { count = count + 1 } } fn makeCounter(): Counter { Counter() } makeCounter().increment() // error: This is a temporary, and `increment` changes its receiver: the change would be thrown awayAs the argument of a
varparameter a temporary is fine: the callee becomes its only owner, so nothing is ever written back anywhere.tick(makeCounter())needs nothing bound first.The variable of a
forloop is aconst; the slot of afor varis a path.for var element in itemsbinds each slot of the container in turn, and every use ofelementis the accessitems[key]of the turn - see Changing elements in place. The variable of a plainforis a copy, so changing an element through it needs the path (items[index].x = 1) or a new collection built withmap.type Counter { var hits: Int } var counters = [Counter(0), Counter(5)] for var counter in counters { counter.hits = counter.hits + 1 } print counters // prints [Counter(hits: 1), Counter(hits: 6)]
What this is not#
Taking a value out of a collection is not a way to reach its path. It is a copy, so changing it changes nothing
the collection holds - this is the copy trap, and reading counters[0] again is the only way to see whether a change
went where it was meant to.
var counters = [Counter(), Counter()]
counters[0].increment() // Through the path: changes the element in place
print counters[0].countvar counters = [Counter(), Counter()]
var first = counters[0] // A copy
first.increment() // Changes the copy. `counters[0]` is still at 0.
print counters[0].countRelated#
- Exclusivity - the rule for when two
varaccesses to the same path may not overlap. - Fields - which field modifier a path is allowed to end on.
- Declaring a type -
var fnon a verb, and its participle that needs no path at all. - Bindings -
constandvaron the binding a path starts from.