Arrays
On this page
Note: Implemented: the array type, with one size or several, array literals, nested arrays, elements given by expressions, a string as an array of u8, construction with contents (new Array[T, n] <- fill x), copying, getting and setting one element by index, each over an array, and a run-time bounds check on every index. Not implemented: the len method (a slice's is), the $- operator, multi-element segments and fills, comparison, and pattern matching. An index out of range is caught only at run time, even when it is a constant.
An array holds a specified number of values, all of the same type. The size of the array (its number of elements) is a fixed, constant value and is not resizable. The array's elements are located next to each other in memory. Elements are individually accessed using an unsigned integer index.
For example:
mut numbers Array[f32, 3] = [3.1, 4.1, 5.92]; imm nbr = numbers[1]; // nbr is 4.1 numbers[0] = 2.; // numbers is now [2., 4.1, 5.92]
Here, numbers is an array containing three floating-point numbers as its elements. As the redundant type specification Array[f32, 3] indicates, the type of its elements is f32 and the length of the array is 3. The [] operator enables access to a specific element.
The array type#
An array type is written Array[T, n]: the element type first, then the size. The size is an integer literal from 0 through 4294967295, and is part of the type: an Array[f32, 3] and an Array[f32, 4] are different types. Array looks like a generic type taking a number, and is built into the language; a module that declares its own Array names that one instead. An array type may be written wherever a type may: a variable, a field, a parameter, a return value, the type a reference refers to, or a generic's type argument (Option[Array[u8, 4]]). In a generic function or method the size may be a number parameter, Array[f32, N], and nothing computed from it (Value Parameters).
An array type may give several sizes, one for each dimension. Such an array is an array of arrays, the first size the outermost, so Array[f32, 2, 3] is the same type as Array[Array[f32, 3], 2]: two rows of three. Its first index picks the row and its second the element in that row, and the elements are stored row by row, the last index varying fastest, as in C's float m[2][3]:
imm matrix Array[f32, 3, 3] = [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]; imm val = matrix[1][1]; // 1.
That is how the array itself is laid out. A library type built over an array decides what its elements mean: geomath's Mat3 holds its nine values column by column in an Array[f32, 9].
Warning: Typically, when you want a collection type capability, you won't reach directly for an array, because of the fixed-size limitations and because you cannot implement methods directly on arrays. More likely, you will use one of the library-based collection types. If you look under the covers of these, you will see these are basically struct types that wrap around a specific array, or a pointer to elements they allocate (as well as possibly other fields), and lend their elements as an array reference. Being structs, they offer implemented methods to work with the collection and its elements.
Initialization#
To create or initialize an array, specify the values of all its elements separated by commas and enclosed within square brackets (as the above example demonstrates). When this array constructor is used to initialize a global variable, the element values must be constants. When used as part of a function's logic, element values may be specified using expressions:
imm pair = [nbr * nbr, nbr + nbr];
An alternative way to create an array of bytes is to fill it from a string literal, where the array's type says how many bytes it is:
imm hello Array[u8, 12] = "Hello world!";
A string is encoded as an sequence of utf-8 code points. The length is the number of bytes it occupies, and the array must be exactly that long: any other length is an error. The type of each element is u8. For C-compatibility, the zero terminator ('\0') is placed at the end of the string, but not counted in its length. A string literal that is not filling an array is not an array: it is a borrowed reference to its text, &str (array references describes how it converts to a slice of bytes, and managed references how it is copied into an owner).
Contents#
An array may also be constructed with its contents: new, its type, then <- and the values that fill it, in order. This is the append operator's list, which any collection takes, and the same forms may be used: values listed, n of x for n values, and fill x for as many as are left to fill. A value that is repeated is evaluated once for each element. This example creates a 100-byte string, where all bytes are spaces:
imm spaces = new Array[u8, 100] <- fill ' '; imm row = new Array[f32, 4] <- 1.0, 3 of 0.0;
An array's size is fixed, so its contents give exactly that many values, and the count before of is a constant, from 0 through 4294967295. An array has no initializer, so the construction takes no parentheses, and a global's contents, like its array literal's elements, are constants.
The contents of an array written with several sizes are its scalars, the values of the element type written first, row by row, in the order the array stores them, so each entry counts scalars and n of x and fill run on across the rows. The contents give exactly as many as the product of the sizes, which may be at most 4294967295. Written nested, the array's elements are its rows, and its contents are those rows, each constructed in turn; inside the outer list, a row's own list is parenthesized. The two spellings are still one type, so either construction is assigned, passed and returned where the other is wanted:
imm grid = new Array[i32, 2, 3] <- 1, 2, 3, 4, 5, 6; // rows [1, 2, 3] and [4, 5, 6] imm cube = new Array[u8, 2, 3, 4] <- fill 0u8; // all 24 bytes imm rows = new Array[Array[i32, 3], 2] <- fill new Array[i32, 3] <- (1, 2, 3);
An alias of an array type keeps the spelling it names. A generic's type parameter stands for a type, not a spelling, so through it an array's contents are its rows. An array written with several sizes whose element type is itself an array, Array[Array[u8, 4], 2, 3], takes no contents: whether they would be its bytes or its four-byte arrays is not decided, so it is written nested to be constructed with contents.
An allocation takes the same contents, and fills the array in the region's memory (managed references).
Copying#
Like numeric values, array values may be passed around between variables and functions:
imm hello Array[u8, 5] = "Hello";
fn main() {
mut dessert = jello(hello);
}
fn jello(mut saying Array[u8, 5]) Array[u8, 5] {
saying = "jello";
saying;
}
In this example, a copy of the hello variable's array is passed to the function jello. It replaces that copy's contents with the string "jello" and returns it to main. The mutable local variable dessert now holds a copy of the array string "jello".
When arrays are passed around, we always make copies of the array (just like we do with numbers). Any changes made to a copy won't change the original array. The exception is an array whose element type has move semantics: such an array moves rather than copies, whether its type is written out or inferred from an array literal.
This may not always be the behavior that you want, though. If instead you want to pass around a reference to an array, such that any changes do change the original array, obtain and pass an array reference.
Getting the length#
To get a count of how many elements are in an array, use the len method:
imm hello Array[u8, 5] = "Hello"; imm length = hello.len; // =5
Indexing#
A single element within an array may be accessed using an unsigned integer index value enclosed in square brackets. The specified index may be an expression. An index of 0 refers to the first element.
Indexing can be used to get or set the specified element:
mut c_array Array[u8, 4] = "coal"; c_array[2] = c_array[1]; // the 3rd element is now the same as the second // c_array now holds the array value "cool"
Array indexing is automatically bounds-checked by its known size. Any attempt to supply an invalid index results in a compile-time or run-time error. At run time it is a panic naming the index, the size and the line: index 4 is out of bounds for a count of 4.
Note: It is also possible to use the ampersand operator to borrow a reference to a specific indexed element in an array.
Indexing from the end#
Sometimes it is more convenient to index an element based on its position relative to the end of the array. This can be accomplished in a verbose way:
imm nbrs = [2, 4, 6]; imm four = nbrs[my.len - 2]; // =4 - the 2nd from the end
The less verbose equivalent uses the $- operator:
imm four = nbrs[$-2];
$- is equivalent to my.len -
Iterating over an array#
Using indexing and the len method, one can now easily iterate over every element in an array:
imm numbers = [3.5, 4.0, 5.5, 3.0];
average = 0.0;
mut i = 0;
while i < numbers.len {
average += numbers[i++];
}
average /= numbers.len; // == 4.0
A cleaner, more concise approach uses an each block instead:
imm numbers = [3.5, 4.0, 5.5, 3.0];
mut average = 0.0;
each nbr in numbers { // nbr borrows each element
average += nbr;
}
average /= 4.0; // the number of elements: == 4.0
Multi-element copying#
Multiple elements of an array may be copied into another array using a range index.
mut greetings Array[u8, 11] = "Hello Jeter"; greetings[7,4] = greetings[1,4]; // "Hello Jello" greetings[6..10] = "Peter"; // "Hello Peter" greetings[6..<11] = "Helen"; // "Hello Helen" greetings[6..] = "Jayne"; // "Hello Jayne" (end is assumed if unspecified)
As the examples show, several range operators are supported. The first number is always the index of the first element in the segment. The meaning of the number after the range operator varies:
- , - The second number specifies how many elements are in the array segment.
- ..< - The second number specifies the index of the element after the last element in the array segment.
- .. - The second number specifies the index of the last element in the array segment. If unspecified, it is assumed to be the last element of the array.
Both array segments must have the same element type and length. Array segments can only be used for copying elements from one array to another using assignment or variable initialization. An array segment cannot be passed as an argument in a function call or used in a comparison.
Note: One can also use the ampersand operator to borrow an array reference (slice) to a specific set of elements in an array.
Multi-element fills#
Using the same range operators, a single value can be repeatedly filled into a segment's elements:
mut numbers = [0, -1, -2, -3]; numbers[1 ..< 3] = 2; // numbers == [0, 2, 2, -3]
The type of the fill value must be the same as the type of the array's elements.
Comparing#
Two arrays may be compared, so long as they have the same element type.
To be equivalent the arrays must have the same length and every element must be the same. To make element comparison possible, the element's type must support the == method.
imm somebody = "John";
if somebody == "Jane" {
....
}
Comparing for order (e.g., <) is also handled by comparing the two arrays element-by-element, stopping the first time two elements are not the same. The number of elements compared is the minimum of the lengths of the two arrays. If the element check shows them as equivalent, the lesser-sized array comes first in order.
Partial pattern match#
The match statement or is operator supports additional array comparison capability, particularly for partial pattern matching.