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.

Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite

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.count

Syntax#

<var-path> ::= (<var-binding> | <var-parameter> | "self" in a var-self member) <step>*
<step>     ::= "." <var-field> | "[" <index-or-range> "]"

Rules#

  1. A var parameter works on the caller's value, and its argument has to be a var path. incrementTwice changes counter itself; there is no marker at the call site, the parameter's own var is what promises it.

    Kompiliert nicht
    type 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`
  2. A path through a[key] or a[from..to] changes the collection in place, without a copy. samples[1..4].sort { value => value } sorts that slice of samples itself, the same way a field step would.

  3. A reference is second-class: it exists only as a var parameter or a var fn receiver, 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.

    Kompiliert nicht
    type Counter {
      var count: Int = 0
    
      var fn watcher(): () => Void {
        { count = count + 1 }
      }
    }
    // error: This closure captures the receiver `self` and may outlive the call
  4. A captured var binding 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.

    Kompiliert nicht
    fn counter(): () => Int {
      var count = 0
      {
        count = count + 1
        count
      }
    }
    // error: This closure captures the `var` binding `count` and may outlive it

    State that has to outlive its scope is a shared type, which says it has an identity; recursion is a local fn, which captures nothing.

  5. 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.

    Kompiliert nicht
    type 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 away

    As the argument of a var parameter a temporary is fine: the callee becomes its only owner, so nothing is ever written back anywhere. tick(makeCounter()) needs nothing bound first.

  6. The variable of a for loop is a const; the slot of a for var is a path. for var element in items binds each slot of the container in turn, and every use of element is the access items[key] of the turn - see Changing elements in place. The variable of a plain for is a copy, so changing an element through it needs the path (items[index].x = 1) or a new collection built with map.

    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].count
var counters = [Counter(), Counter()]
var first = counters[0]          // A copy
first.increment()                // Changes the copy. `counters[0]` is still at 0.
print counters[0].count
  • Exclusivity - the rule for when two var accesses to the same path may not overlap.
  • Fields - which field modifier a path is allowed to end on.
  • Declaring a type - var fn on a verb, and its participle that needs no path at all.
  • Bindings - const and var on the binding a path starts from.
Esc