8.8. Vectors
Vectors are language-supported objects that provide runtime-sized arrays.
Unlike an array, whose length is fixed once at its initialization
(see Arrays Versus Vectors), a vector is runtime-sized: it begins
at some length and may grow over its lifetime through its mutating methods
(push and append).
Once created, vectors in Gazprea interoperate with arrays for the
element types they both support: they can be intermixed with arrays in
expressions; they can be used on the RHS of array declarations and
initializations; and they can be passed as array arguments to functions
and procedures. When a vector appears in an expression it is used as an
array value of its current length. Vectors are nevertheless a distinct
type, we will explore the differences below.
8.8.1. Declaration
Vectors are declared and (optionally) initialized as follows, where
qualifier, elem-type, id, and value are placeholders (the angle
brackets of vector<...> are literal):
[qualifier] vector<elem-type> id; [qualifier] vector<elem-type> id = value; [qualifier] vector<elem-type> id = array-value;
Unlike the array type, Gazprea vectors do not have an explicit size specifier. Also, vectors do not have a defined pre-allocated or suggested memory extent, often called capacity in other languages.
The element type T of a vector<T> may be any storable type.
Below are some examples of vector declarations.
const vector<integer[2]> v1 = 3; // [[3, 3]] const vector<integer[2]> v2 = [4, 5]; // [[4, 5]] const vector<integer> v3 = 42; // [42] var vector<integer> v4 = 42; // [42], mutable vector<integer> v5 = 42; // [42], implied const const vector<real> v6 = 1; // [1.0]
A vector declaration vector<T> v = E is resolved in one of two ways,
chosen by the rank of the right-hand side E relative to the element type
T:
Single-element declaration –
Eis a scalar, or an array of the same rank asT, and is implicitly cast or broadcast toT. The vector then has exactly one element:Econverted toT. A scalar is broadcast to fill that element; a same-rank array is cast toTelement-wise and, whenTis a fixed-size array, fitted toT’s size by the usual array-to-array rules; a shorter value is padded with the element type’s zero value, a longer one is aSizeError. Sovector<integer[2]> v = [4, 5]is the one-element[[4, 5]], andvector<integer[3]> v = [4, 5]is the one-element[[4, 5, 0]](theinteger[2]value is padded tointeger[3]).Multi-element declaration –
Ehas the rank of the vector’s underlying array typeT[], one rank higher thanT. Each element ofEmust be implicitly castable toT(see Implicit Casts); the vector holds those elements, in order, each converted toT.
Any other rank of E is a TypeError (see Errors). A scalar is
always the single-element case, so vector<integer> v = 42 is [42]; to
supply several elements you write the literal one rank deeper. The two spellings
can therefore denote the same value: for const vector<integer[*]> a = [1, 2]
the right-hand side is a single integer[*] element, so a == [[1, 2]],
while const vector<integer[*]> a = [[1, 2]] is a multi-element declaration
with one element, so a == [[1, 2]] as well. When T is an inferred-size
array (T[*]), the element(s) selected by whichever case applies fix that
inferred size once, as described next.
A vector<T[*]> – a vector whose element is an inferred-size array – fixes
that element size (the *) exactly once, from the first array value that
enters the vector, and fits every later element to it: a shorter array is
padded with the element type’s zero value, and a longer one raises a
SizeError (see Errors). What counts as the “first value” depends
on how the vector is populated, and the two paths must not be conflated:
Initialized from an array value (including a nested array literal): the right-hand side is evaluated to an array value on its own first, and only then stored. A nested literal such as
[[1.0], [2.0, 3.0]]is an ordinary array literal, so it is normalized to a rectangle by padding every sub-array to the longest one exactly as in matrix construction. Once normalized, this padding is a property of the literal, so it is identical whether the literal initializes an array variable or a vector.Built up incrementally with
push/appendfrom a shorter or empty vector, so the first element stored fixes the size and each later element is fitted to it.
A vector of arrays is therefore never ragged: once the element size is fixed,
every element has that shape. A vector<vector<T>>, by contrast, may be
ragged, because each inner vector carries its own runtime length and no element
imposes its shape on the others.
Because a nested literal is padded to its longest sub-array before it is stored, neither initializer below is ragged and neither is an error; the short sub-array is simply padded, whichever side it is on:
const vector<character> vec = ['a', 'b', 'c']; // ['a', 'b', 'c'] // Each RHS is normalized to its longest sub-array, then stored: const vector<real[*]> x = [[1.0], [2.0, 3.0]]; // x == [[1.0, 0.0], [2.0, 3.0]] const vector<real[*]> w = [[1.0, 2.0], [1.0]]; // w == [[1.0, 2.0], [1.0, 0.0]] const vector<character> const_vec = vec; // copy of vec
Growing a vector one element at a time is different: there is no surrounding
literal to normalize, so the first stored element fixes the size and each later
element is fitted to it. This is why the same value pads differently depending
on the path – x above is padded as a whole literal, whereas y below
pads only its newly pushed element:
var vector<real[*]> y = [[1.0, 2.0]]; // element size fixed at 2 call y.push([3.0]); // [3.0] padded to [3.0, 0.0] // y == [[1.0, 2.0], [3.0, 0.0]]
An initially empty vector<T[*]> takes its element size from the first array
appended, after which the usual pad / SizeError rules apply. The first
append below fixes the element size at 2; each later line is an independent
continuation from that size-2 state (shown separately so the SizeError line
does not abort the ones after it):
var vector<integer[*]> z; // empty; element size not yet fixed call z.append([1, 2]); // first element fixes the size at 2: z == [[1, 2]] call z.append([1]); // shorter: padded to [1, 0] call z.append([1, 2, 3]); // longer than the fixed size 2: SizeError call z.append(1); // scalar 1 broadcasts to [1, 1], then appended
8.8.2. Operations
Operations on vectors use the same syntax as operations on arrays and,
except for the differences enumerated above, share their semantics: in an
expression a vector is treated as an array value of its current length.
In particular, operand lengths must match for binary expressions and dot
product. Every element-wise binary operation with a vector operand – whether
the other operand is a vector or an array – produces an array result;
vector-ness is never propagated through those operators, and the resulting array
may of course be implicitly cast back to a vector (or string) when it is
stored into one (see Array to/from Vector). Concatenation with
|| is the exception: it is right-associative and its result takes the kind
of its receiver, the rightmost operand, so a concatenation whose receiver is a
vector is itself a vector – in particular a string concatenation stays a
string (see Operations).
Operator precedence and associativity are specified once, for all types, in the table of operator precedence.
8.8.3. Method Calls
As a language-supported object, Gazprea provides methods for vector
(and therefore for the typealias string, which is just
vector<character>). A method call has the form
receiver.method(arguments) and is governed by the following rules:
Each method is either a function or a procedure, according to whether it observes the receiver or mutates it. A stateless method such as
lenis a function: it is pure, returns a value, and does not change the receiver. A stateful method such aspushorappendis a procedure: it mutates the receiver. A methodm(args)invoked on avector<T>receiver behaves exactly as a call tofunction m(vector<T> self, args...) returns U(stateless) orprocedure m(var vector<T> self, args...)(stateful): the receiver is bound toselfand the call has ordinary function- or procedure-call semantics. Onlyvector(and thusstring, its typealias) has methods in this version of the language; user-defined methods onstructtypes are a future extension.The receiver must be a variable of a language-supported object type (
vectororstring). Arrays, array slices, and the (array-valued) results of expressions have no methods; calling a method on them is a compile timeTypeError(see Errors).A function method (such as
len) is an expression: its result is a value, so it may appear in any expression position – on the right of a declaration or assignment, as an argument, or in an output-stream expression such asv.len() -> std_output. Like any function call it may not stand alone as a statement, andcalldoes not apply to it.A procedure method (such as
pushandappend) is used as a statement and, like any other procedure call, must be written as acallstatement:call v.push(1);. Written without thecallkeyword – a barev.push(1);– it is a CallError.Mutating methods (
push,append) additionally require the receiver to be declaredvar. Inside a function, mutating methods may be applied only to variables local to the function; this preserves function purity, since no state outside the function can change.
You may find it useful to think of vectors similar to a struct with an impl follows (Note: this is illustrative notation, impls do not exist in the gazprea 26 standard):
struct vector(T[] data, integer len) impl { procedure push(var self, T other); procedure append(var self, T[] other); function len(self) returns integer; }
The methods are:
push(x)(procedure) - pushesxonto the back of the vector as a single new element;xis cast to the element typeTexactly as in the single-element case ofappendand of a vector declaration (a scalar broadcasts, a shorter array pads)len()(function) - number of elements in the vectorappend(x)(procedure) - append to the vector, whereTis the element type.xis split into elements ofTby the same single-versus-multi test as a vector declaration (see Declaration): ifxis a scalar or an array of the same rank asTit is cast toTand appended as a single element; ifxhas the rank ofT[](one higher thanT) each of its elements is cast toTand they are appended in order. The two cases are mutually exclusive, so no tie-break is needed.var vector<integer> v1; // v1 == [] v1.len() -> std_output; // 0 call v1.push(1); // v1 == [1] v1.len() -> std_output; // 1 call v1.push(2); // v1 == [1, 2] v1.len() -> std_output; // 2 call v1.append([3, 4, 5]); // v1 == [1, 2, 3, 4, 5] v1.len() -> std_output; // 5 var vector<real[2]> v2; // v2 == [] const x = 1..10; // `1` is implicitly cast to `[1.0, 1.0]` before appending call v2.append(1); // v2 == [[1.0, 1.0]] // length 1 array padded to length 2 call v2.append([3.0]); // v2 == [[1.0, 1.0], [3.0, 0.0]] // slices call v2.append(x[5..6]); // v2 == [[1.0, 1.0], [3.0, 0.0], [5.0, 6.0]] v2.len() -> std_output; // 3 call (v1 + v1).push(3); // TypeError: the sum is an array // value, and arrays have no methods
Slicing a vector produces an array slice (there are no “vector slices”).
var vector<integer> v3 = x; call v3[2..5].append(x[5..6]); // TypeError; cannot do `append` on an array slice