Array References and Slices
On this page
Note: Not implemented: a subslice's count form (,), an index relative to the end ($-), a slice made from a reference or a pointer, and copying and filling a range of elements. A borrowed array or string literal (&[1, 2, 3], &"Padma") is not yet a slice: it is a reference to the array, with no .len of its own.
An array reference, or slice, is a borrowed view of some number of elements of the same type, laid out one after another in memory. How many there are is known only at runtime, so the reference carries the count alongside the pointer to the first element. A slice does not own its elements: it is borrowed from something that does, and like any borrowed reference it may not outlive it.
Owned Arrays#
The body of a run-time length is Array[T], the array type without a size. It has no size of its own, so it is held only through a reference that carries the count, and the slice is its borrow: &Array[T] is the slice, and &mut Array[T] the slice that changes its elements; the earlier spelling, &[]T, is refused. Every array reference is borrowed. There is no owning array reference: a managed reference is thin, virtual, or fat with the count of a dynamically sized body, str or Array[T], never an array reference. A slice of bytes, &Array[u8], and a borrow of the text body, &str, are the same pair of words and different types, and either converts to the other with as. What owns an array is one of these:
- A fixed-size array, whose size is part of its type.
It may be held in a variable or a field, or allocated in a region and reached by a thin reference
whose type carries the size. Its contents follow the allocation after
<-, and fill the array in the region's memory, with no array
made elsewhere and copied in; an array already made is moved in from the parentheses:
imm grid = new Rc[mut, Array[i32, 4]] <- fill 0i32; // grid's type: Rc[mut, Array[i32, 4]] imm made = new Rc[mut, Array[i32, 4]](new Array[i32, 4] <- 1, 2, 3, 4);
- An owner of Array[T], when the number of elements is chosen at runtime
and never changes: So[Array[T]], Rc[Array[T]],
Arc[Array[T]] or Gc[Array[T]], each a pointer and a count,
one hop to the elements. It is allocated by copying the elements of a slice, as an owner of
str is, so only where the elements copy; a list's
freeze() moves its own into one instead, any element included. The owner has
len, indexes (o[i]) and takes a borrowed range
(&o[a..<b]) as a slice does, and is lent as the slice wherever a
&Array[T] is wanted, a generic's element type inferred from it. Which of the
three kinds of list it is comes from its permission: held writable
(So[Array[T]], Rc[mut, Array[T]]) its elements change but
its length does not; held imm (Rc[imm, Array[T]],
Arc[imm, Array[T]]) nothing changes. A borrow of a fixed-size array,
&Array[T, n], converts to the slice.
imm primes = new Array[i32, 4] <- 2, 3, 5, 7; imm shared = new Rc[imm, Array[i32]](&primes); // a copy: a pointer and a count imm total = sum(shared); // lent as the &Array[i32] sum takes imm frozen = list.freeze(); // a List[T]'s block, moved into a So[Array[T]]
- A collection, when the number of elements is chosen at runtime.
An array whose whole length is decided at runtime, and which may grow or shrink,
is exactly what a collection is: a List holds a pointer to its elements,
how many there are, and how many it has room for.
A list held in a local is used directly; one that several owners share is a managed reference to it:
imm names = new List[i32] <- 3; imm shared = new Rc[mut, List[i32]](names); // names is moved in; shared's type: Rc[mut, List[i32]]
The length of a shared list lives once, in the list, where every owner sees the same count. A length carried in each copy of a reference could not stay in step when the list grows. Writing +[], as a type or an allocation, is an error that says this.
Borrowing a Slice#
A slice may be borrowed from any array, from another slice, or from a collection that lends one:
imm nbrSlice = &[1, 2, 3]; imm textSlice = &"Padma"; imm listSlice = list.view(); // from a List imm fillSlice = list.viewMut(); // from a List, its elements changeable: &mut Array[T]
These slices point to all elements of the source's array. (The first two borrow a literal: see below for how that differs from a slice made with &[].)
If a static array is passed as a call argument where a slice is expected, it is automatically converted into a slice:
print("abc"); // equivalent to print(&"abc")
Safety Note: Because a slice is a borrowed reference, its lifetime is limited, ensuring it can never outlive the source array it provides a view into. Depending on permissions, it may temporarily lock out access to the variable it borrows from, thereby ensuring the original array cannot be moved or resized while the slice is active.
Borrowing a Literal or a Constant#
A literal whose elements are all constants, an array literal or a string literal, is kept in read-only memory, and so is a named constant holding an array literal or the byte array a string literal fills. A borrow of one is a constant too: it lives as long as the program, so a function may return it, and it may not be mut.
Written &[1, 2, 3] or &"Padma", the borrow is a reference to the array (&Array[i32, 3]). It is taken wherever a slice is wanted, but it has no .len of its own. &[][1, 2, 3] is the slice, with one:
const primes Array[u32, 4] = [2u32, 3u32, 5u32, 7u32];
fn firstOf(p &Array[u32]) u32 { p[0]; }
fn smalls() &Array[i32] { &[1, 2, 3]; } // the borrow outlives the call
imm nums &Array[i32, 3] = &[1, 2, 3]; // a reference to the array: nums.len is refused
imm sl &Array[i32] = &[][1, 2, 3]; // the slice: sl.len is 3
imm two = firstOf(&primes); // a named constant, borrowed: 2
A String as a Slice of Bytes#
A string literal on its own is not an array but a &str, a borrow of its text that lasts as long as the program (managed references). Any &str converts to a slice of bytes, &Array[u8], wherever one is wanted, so a literal goes where a slice of bytes is taken. The way back is written, bytes as &str, since bytes need not be UTF-8:
fn say(bytes &Array[u8]) { printStr(bytes as &str); }
say("Padma"); // the literal, as a slice of bytes
Borrowing a Computed Literal#
An array literal with a computed element, such as &[x, y], is a temporary. It lives to the end of its statement, or, where a variable's initializer borrows it, to the end of the block (see borrowing a temporary). A borrow of it cannot be returned from the function:
imm s = &[][x, y]; // lives to the end of the block imm t = total(&[x, y, 6]); // lives to the end of the statement
Subslices#
It is also possible to create a new slice that points to some subset of the elements in some array or slice. This is accomplished by indexing using a range operator between two unsigned integers:
- .. includes the last specified element.
- ..< stops before the last specified element.
- , specifies how many elements are in the slice.
The two range operators are the ones a number range of each and a range pattern use. For example:
imm x = &"Abcd"[1..<3]; // a slice pointing to "bc" imm y = &"Abcd"[1..3]; // a slice pointing to "bcd" imm z = &"Abcd"[2, 1]; // a slice pointing to "c"
If either index integer is unspecified, 0 and end are assumed (so [..3] is the elements 0 through 3, [..<3] the first three, and [1..] everything from 1 on). A range is checked against the length of what it is taken from, as an index is: its start may not come after its end, nor its end after the last element. The subslice has the permission and lifetime a borrowed reference to one of those elements would have.
imm r = &y[1..]; // a slice pointing to "cd" (end assumed) imm s = &y[..]; // a slice pointing to "bcd" (0 and end assumed)
An index integer may be specified relative to the last element:
imm r = &y[$-2..]; // a slice pointing to "cd"
A slice may also be created from a regular reference or a pointer:
imm slice1 = &ref[..]; // Slice count from reference is always 1 imm slice2 = &ptr[0..5];
Operations#
Slices may be indexed, copied, compared, iterated over, and queried for their properties. Many of these operations work the same way they do with static arrays. A slice, and an array, calls the methods of core's Array[T]: iter(), indexed(), mutItems() (a &mut slice's, for changing elements in place), chunks(n) and mutChunks(n) (the elements in runs of n, each run a slice, a &mut one from mutChunks), which each walks.
Access an Element#
A single element within an array may be accessed using an unsigned integer value enclosed in square brackets. This supports both getting and setting the values at a specific index:
c_array[3] = c_array[2]; // the 4th element is now the same as the third
Indexing is automatically bounds-checked by its known size. An index out of bounds is a panic naming the index, the size and the line; so is a range, &s[a..<b], that starts after it ends or ends past the size.
Borrowed Reference to an Element#
One can create a borrowed reference to any element of a slice in the same way as for an static array:
imm nbrRef = &nbrSlice[1]; // points to 2 imm charRef = &textSlice[2]; // points to 'd'
Borrowing a reference to a single element is not a slice. It is just a regular borrowed reference to some value.
Copy or Fill Elements#
Multiple elements of an array may be copied into another array using a range in square brackets. Both content segments must have the same type and size.
arrref[1 by 2] = sliceref[0 by 2]; greetings[n by 5] = "Kevin";
Similarly, a single value can be repeatedly filled into a slice's elements:
arrref[1 ..< 3] = 2;
Iteration#
The each loop may iterate over a slice's elements:
mut sum = 0;
each x in intslice { // x borrows each element
sum += x;
}
Each pass's variable is a borrow of the element, which cannot be changed through it. Walking mutItems() instead lends each element as a &mut to change it where it lies, and indexed() pairs each element with its position:
each x in mutslice.mutItems() {
*x = *x + 1;
}
each i, x in intslice.indexed() { // i is a usize
sum += i32.from(i) * x;
}
Both are counted loops over the slice, as the plain each is (Each and Iteration). A bare indexed() lends & on any slice, a &mut one too; mutslice.mutItems().indexed() lends each element as a &mut. chunks(n) and mutChunks(n) hand the elements out in runs of n, each run a slice (the last is what is left; n of 0 is a panic), and a parallel each gives each pass a run of its own:
each run in intslice.chunks(4) { ... } // run is a slice of up to 4 elements
each y, row in mutslice.mutChunks(w).indexed() {
row[0] = y; // row is a &mut slice
}
Seen as Rows: asArrays#
asArrays[N]() sees a slice of T as a slice of Array[T, N] over the same memory: nothing is copied, and element k of row i is element i * N + k of the slice. N is a number parameter written in brackets, since no argument says it. mutArrays[N]() is the same on a &mut slice and gives a &mut one, so writing a row's element changes the original (it has a name of its own, as mutChunks has beside chunks). Both are methods of an array, a slice and a list. A count that is not a multiple of N, and an N of 0, are a panic at the line of the call.
imm bytes = &[][1u8, 2u8, 3u8, 4u8, 5u8, 6u8]; imm rows = bytes.asArrays[3](); // &Array[Array[u8, 3]]: [1 2 3] [4 5 6] imm c = rows[1][2]; // 6 pixels.mutArrays[3]()[0][1] = 9u8; // changes pixels[1]
The rows are a slice like any other, so the runs cursors cut them: a picture of bytes, three to a pixel, is written a row to a pass, a pixel's channels by index, and leaves the bytes the same loop leaves when it computes x * 3 + k itself:
parallel each y, line in pixels.mutArrays[3]().mutChunks(width).indexed() {
each x in 0usize ..< width {
line[x][0] = red(x, y);
line[x][1] = green(x, y);
line[x][2] = blue(x, y);
}
}
Comparison#
Two slices may be compared for identity with === (or !==), so long as they have the same type. This comparison checks for equality of both the starting pointer and the element count, so it asks whether the two view the same elements, not whether their elements are equal.
if slice1 === slice2 {
....
}
As with other references, == (or !=) on two slices compares what they refer to, element by element. Two slices are equal when they have the same number of elements and each element is == to the element at the same index in the other. Each pair is compared with the element type's own ==: a number's or a bool's, or one a struct declares; a slice of slices compares each inner slice the same way. Floating point elements compare as floating point numbers do, so a NaN is not equal to any element, itself included, and -0.0 is equal to 0.0. A slice whose element type has no == cannot be compared this way.
imm name &Array[u8] = "Padma";
if name == "Padma" { // true: the same five bytes
....
}
What a slice is compared with may be anything that would be accepted where that slice is wanted: another slice, an array or a reference to one, or a string literal. An array or a reference to one may be on the left too.
Comparing for order (e.g., <) is not possible directly. One can of course compare the component parts of a slice separately.
Property Queries#
A slice is a "fat" reference, as it holds two pieces of information:
- a pointer to the first element of the array
- an unsigned integer specifying how many elements are in the array
The component parts of a slice can be obtained:
imm ptr = array.ptr; // The pointer to the first element imm len = array.len; // The number of elements
Collection Types#
A collection type owns its elements and lends them out. A List keeps its elements in one block of memory it allocates and grows itself, reached through a pointer beside its count and its capacity, and lends them as a slice (view()). It defines operator methods (such as indexing), so that it behaves very much like an array.