Sequences · Lesson 5.5

Writable slice

Source
Change an array's elements through a writable view, var int32[..], and see that a view's writability is separate from its binding's mutability.
You'll need: Slice

A plain slice, int32[..], is read-only: it can look at the elements but never change them. A writable slice, var int32[..], may also write through to the array it views. That is how a function changes the elements of an array it was given — fill it, sort it, scale it — without copying it first.

Two kinds of view

Which kind you get depends on the array you take it from:

Taken fromExampleTypeCan write elements?
a var arraystorage[1..4]var int32[..]yes
a let arraynumbers[1..4]int32[..]no
any array, passedShow("…", storage)int32[..]no

A writable view writes straight into the array behind it:

var storage: int32[5] = [1, 2, 3, 4, 5];
let view = storage[1..4];
view[0] = 20;

view starts at index 1 of storage, so view[0] = 20 changes storage[1] from 2 to 20.

A function that changes its argument

A function that writes into its argument's elements asks for a writable view:

func Fill(values: var int32[..], value: int32) {
    for i in 0..values.length {
        values[i] = value;
    }
}

A function that only reads asks for a read-only view — and a writable one is accepted there too, so Show takes both.

There is one asymmetry. An array becomes a read-only view on its own, but a writable one has to be asked for with a range, so that changing the caller's array is always visible at the call:

Fill(storage[3..], 0);

storage[..] would fill all of it.

The view and its binding

The surprise is that a view's writability has nothing to do with how the view itself is bound. Two separate questions are being answered:

flowchart LR
    b["the binding<br/>let or var"] -- "can it be pointed<br/>somewhere else?" --> v["the view<br/>int32[..] or var int32[..]"]
    v -- "can it change<br/>the elements?" --> a["the array"]

let view = storage[1..4]; is a writable view in an immutable binding: view[0] = 20 is fine, because it changes the array, but view = … is rejected, because it would change the binding. A var binding of a read-only view is the reverse — it may move along the array, but never write an element:

var cursor: int32[..] = storage[..2];
PrintLine("cursor  starts at {}", cursor[0]);
cursor = storage[2..];
BindingViewPoint it elsewhere?Write elements?
letint32[..]nono
letvar int32[..]noyes
varint32[..]yesno
varvar int32[..]yesyes

Views see later writes

A writable view may be stored as a read-only one. Both look at the same array, so the read-only view sees what is written through the other afterwards:

let reader: int32[..] = view;
view[1] = 30;
PrintLine("reader  sees {}", reader[1]);

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A plain slice, `int32[..]`, is read-only: it can look at the elements but never change them.
// A writable slice, `var int32[..]`, may also write through to the array it views. Taking a
// range of a `var` array gives a writable view; taking one of a `let` array gives a read-only
// one.
//
// The surprise is that the view's writability has nothing to do with how the view itself is
// bound. `let view = storage[..];` is a writable view held in an immutable binding: `view[0] = 1`
// is fine, because it changes the array, while `view = ...` is rejected, because it would change
// the binding. A `var` binding of a read-only view is the reverse: it can be pointed somewhere
// else, but it can never write an element.
import Io::PrintLine;

// A function that changes its argument's elements asks for a writable view.
func Fill(values: var int32[..], value: int32) {
    for i in 0..values.length {
        values[i] = value;
    }
}

// A function that only reads asks for a read-only view, and a writable one is accepted too.
func Show(label: char8[..], values: int32[..]) {
    PrintLine("{} {} {} {} {} {}", label, values[0], values[1], values[2], values[3], values[4]);
}

func Main() -> int {
    var storage: int32[5] = [1, 2, 3, 4, 5];
    Show("start  ", storage);

    // A `let` binding, a writable view. Writing through it writes the array.
    let view = storage[1..4];
    view[0] = 20;
    Show("view   ", storage);

    // The function writes through its view, so the change shows up in `storage`. An array
    // becomes a read-only view on its own, but a writable one has to be asked for with a range.
    // `Fill(storage[..], 0)` would fill all of it, while `Fill(storage, 0)` stops with
    //     error: no matching overload for 'Fill' with argument types (int32[5], int)
    Fill(storage[3..], 0);
    Show("filled ", storage);

    // A `var` binding, a read-only view. It may move along the array, but `cursor[0] = 9`
    // would be rejected.
    var cursor: int32[..] = storage[..2];
    PrintLine("cursor  starts at {}", cursor[0]);
    cursor = storage[2..];
    PrintLine("cursor  moved to {}", cursor[0]);

    // A writable view may be stored as a read-only one, which then sees later writes too.
    let reader: int32[..] = view;
    view[1] = 30;
    PrintLine("reader  sees {}", reader[1]);
    return 0;
}

Run it

cd Examples/Sequences/WritableSlice
rux run
start   1 2 3 4 5
view    1 20 3 4 5
filled  1 20 3 0 0
cursor  starts at 1
cursor  moved to 3
reader  sees 30

Common mistakes

Passing the array itself to a writable parameter.
Fill(storage, 0) fails with error: no matching overload for 'Fill' with argument types (int32[5], int). An array only becomes a read-only view on its own; write Fill(storage[..], 0) to hand over a writable one.
Taking a writable view of a let array.
With let fixed: int32[3] = [1, 2, 3];, the call Fill(fixed[..], 0) fails with has type 'int32[..]', but parameter 'values' requires 'var int32[..]'. A view can never grant more than the array allows.
Confusing the binding with the view.
cursor[0] = 9; fails with error: cannot modify elements through read-only slice 'int32[..]', even though cursor is var. And view = storage[..2]; fails with error: cannot modify immutable variable 'view', even though view can write elements.

Try it yourself

  1. Write Scale(values: var int32[..], factor: int32) that multiplies every element, and call it on storage[..].
  2. Write Reverse(values: var int32[..]) that swaps elements from both ends towards the middle.
  3. Call Fill(storage, 0) and read the error, then fix the call.
  4. Change storage to a let array and see which lines stop compiling.

Learn more