LEVIATHAN v962456e · 962456eee1

Library

Collections are values

Array, Map and Set never change in place; every changing method returns a new collection, and the idiom for change is to rebind.

since 0.1.0-alpha.1linuxwindowswasm

Description

Array<T>, Map<K, V> and Set<T> are pure values. A method that appears to change one of them, such as add, with, without, insertAt or removeAt, builds and returns a new collection and leaves the receiver untouched. To "change" a collection you rebind the variable that holds it:

Array<int> xs = [1, 2];
xs = xs.add(3);
Map<string, int> ages = Map();
ages = ages.with("ann", 30);
Set<int> seen = Set();
seen = seen.with(7);

The bracket assignments xs[i] = v and m[k] = v are the same rebind written in shorter form: they assign a new collection to the variable xs or m.

This has three consequences.

  • A collection you pass to a function is safe. The function receives its own value. Anything it does to its parameter is visible only inside the function, and the caller's variable is unchanged. A function that wants to hand a changed collection back returns it.
  • Copying is free to the program. Array<int> b = a; gives b its own value. Changing b later never shows through a, and the other way round. The implementation shares storage and copies lazily (copy on write), but that is an optimisation you cannot observe; the language guarantees only the value behaviour.
  • A call that discards its result does nothing. xs.add(3); as a statement compiles and has no effect, because the new array is dropped. Always assign the result.

The same rule for Array, Map and Set

void grow(Array<int> xs) {
    xs[0] = 100;
    xs = xs.add(1);
    console.writeln(xs);
}
Array<int> a = [1, 2];
grow(a);
console.writeln(a);
Map<string, int> m = Map();
m = m.with("a", 1);
Map<string, int> copy = m;
copy["a"] = 50;
copy = copy.with("b", 2);
console.writeln(m);
console.writeln(copy);
Set<int> s = Set([1, 2]);
Set<int> t = s.with(3);
console.writeln(s.toString());
console.writeln(t.toString());
[100, 2, 1]
[1, 2]
{a: 1}
{a: 50, b: 2}
{1, 2}
{1, 2, 3}

Rules

  • No method of Array, Map or Set mutates its receiver. A method that changes contents returns a new collection.
  • xs[i] = v and m[k] = v rebind the variable; they do not write into shared storage.
  • Passing a collection to a function, assigning it to another variable, or storing it in a field never lets a later change through one name show through another.
  • The value rule covers the collection itself, not the objects inside it. A collection of class instances holds references to those objects: a new collection made from it points at the same instances, and a change made to an instance through one collection is seen through the other. Struct elements are values: a[i] gives a copy, so to change a struct element build a new struct and store it back with a[i] = P(...).
  • Map and Set keep insertion order. Replacing the value of an existing key in a Map keeps that key in its original position; removing a key and adding it again puts it at the end.

Examples

Objects inside a collection stay shared

class Counter {
    int n = 0;
    void bump() { n = n + 1; }
}
Counter c = Counter();
Array<Counter> original = [c];
Array<Counter> longer = original.add(Counter());
longer[0].bump();
console.writeln(original[0].n);
console.writeln(longer.length());
console.writeln(original.length());
1
2
1

Notes

Collections stored in a class field follow the same rule: the field is a variable, and a method that changes the collection assigns the new value back to the field, for example items = items.add(x);.

See also

  • Array — An ordered sequence of values of one type, Array<T>, with value semantics.
  • Map — An associative collection from keys of type K to values of type V, with value semantics.
  • Set — A collection of distinct values of type T, with value semantics.