8.10. Matrices
Gazprea arrays generalize to arbitrary rank: a type of the form
T[n1][n2]...[nk] is a rank-k array whose element type T may be
any storable type. A matrix is the rank-2
case, and this section describes it in full; higher-rank arrays follow the
same construction, indexing, and element-wise operation rules, generalized
to k index positions. The rows and columns built-ins discussed
below are defined on matrices (rank-2 arrays) specifically, and length on
rank-1 arrays; there is currently no size query for arrays of rank 3 or
more, so their extents are not observable at run time. This is a known
limitation: a general shape built-in reporting the extents of an array of
any rank is planned for a future revision of this specification. Matrix
multiplication (**), by contrast, is defined for
arrays of any rank, as described in Operations.
8.10.1. Declaration
Matrix declarations are similar to array declarations, the difference being that matrices have two dimensions instead of one. The following are valid matrix declarations:
integer[*][*] A = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
integer[3][2] B = [[1, 2], [4, 5], [7, 8]];
integer[3][*] C = [[1, 2], [4, 5], [7, 8]];
integer[*][2] D = [[1, 2], [4, 5], [7, 8]];
integer[*][*] E = [[1, 2], [4, 5], [7, 8]];
Both matrix dimensions are initialization-time sized: each length is
fixed once when the matrix is initialized and never
changes thereafter. A [*] in either position infers that dimension once
from the initializer, exactly as [*] infers the length of a 1-D array.
The extent of a matrix or n-d array is fixed at initialization and cannot be
changed.
8.10.2. Construction
A 2D matrix can be viewed as an array of arrays.
The elements in each array form a single row of the matrix.
All rows with fewer elements than the row of maximum row length are padded with
the element type’s zero value on the right. Similarly, if the matrix is
declared with more rows than are provided, the bottom rows hold the element
type’s zero value. If the number
of rows or columns exceeds the
amounts given in a declaration the compiler must emit a SizeError
(see Errors) at compile time or run time.
This pad-to-longest-row rule is a property of the nested array literal itself, so
it applies identically whether the literal initializes a matrix, an array
variable, or a vector of arrays. Only incrementally
growing a vector with push/append behaves differently, fitting each new
element to the size fixed by the first element; Vectors walks through
the contrast with worked examples.
integer[*] v = [1, 2, 3];
integer[*][*] A = [v, [1, 2]];
/* A == [[1, 2, 3], [1, 2, 0]] */
Similarly, we can have:
integer[*] v = [1, 2, 3];
integer[3][3] A = [v, [1, 2]];
/* A == [[1, 2, 3], [1, 2, 0], [0, 0, 0]] */
Also matrices can be initialized with a scalar value.
Initializing with a scalar value makes every element of the matrix equal
to the scalar. This requires the matrix’s dimensions to be given explicitly in
every position: a matrix with an inferred [*] dimension has no shape to
infer from a scalar, so initializing a matrix that has any [*] dimension
from a scalar is ill-formed, and the compiler must emit an
AssignError (see Errors).
integer[3][3] A = 5; /* fine: every element is 5 */
integer[*][*] B = 5; /* AssignError: no shape to infer from a scalar */
Gazprea supports empty matrices. A rank-2 array initialized from the empty
literal [] is the empty rank-2 array, written [[]]:
integer[*][*] m = []; /* m == [[]], an empty rank-2 array */
Like an empty 1-D array, an empty matrix has its (zero) dimensions fixed at
initialization and is not growable. Both of its dimensions are zero:
rows(m) and columns(m) are each 0 (a 0x0 matrix, notwithstanding the
[[]] notation).
8.10.3. Operations
Multi-dimensional arrays have binary and unary operations of the element type defined in the same manner as uni-dimensional arrays. Unary operations are applied to every element of the matrix, and binary operations are applied between elements with the same position in the arrays.
The operators ==, and != also have the same behavior independent of the dimensionality of the array. These operations compare whether or not all elements of the two matrices are equal.
Two dimensional arrays have several special operations defined on them.
If the element type is numeric (supports addition and multiplication),
then matrix multiplication is supported using the operator **.
Matrix multiplication is only defined between matrices with compatible element
types, and the dimensions of the matrices must be valid for performing matrix
multiplication. When the two operands have differing element types (e.g.
integer ** real), each element is implicitly cast to a common type (see
Implicit Casts) before multiplication, just as for element-wise
binary operations.
Specifically, the number of columns of the first operand must equal the number
of rows of the second operand, e.g. an \(m \times n\) matrix multiplied by
an \(n \times p\) matrix will produce an \(m \times p\) matrix.
If the dimensions are not correct the compiler must emit a SizeError
(see Errors).
When one operand of ** is a scalar it can be broadcast to a matrix operand
of matrix multiplication only if the other operand is a square matrix: a
scalar s paired with an \(n \times n\) matrix is filled into an
\(n \times n\) matrix whose every element is s before the
multiplication. If the other operand is not square the scalar cannot be
broadcast and the compiler must emit a TypeError (see Errors).
Scalars broadcast this way only to ** operands whose extents are all equal –
a rank-1 array (trivially, since it has a single extent, so a scalar may be
dotted with a vector; see the dot product), a square
matrix, or a higher-rank hypercube with equal extents; Gazprea does not
provide comprehensive broadcasting. See Implicit Casts.
More generally, ** is defined for numeric arrays of any rank as the
single-axis contraction familiar from linear algebra: the last dimension of
the left operand is contracted with the first dimension of the right
operand. Writing the left operand as a rank-\(a\) array A and the right
operand as a rank-\(b\) array B, the last extent of A must equal
the first extent of B – otherwise the compiler must emit a SizeError
(see Errors) – and the result C has rank \(a + b - 2\),
given by
\(C[i_1 \ldots i_{a-1},\, k_2 \ldots k_b] = \sum_j A[i_1 \ldots i_{a-1},\, j] \cdot B[j,\, k_2 \ldots k_b]\).
The rank-1-with-rank-1 case is therefore the dot product (a scalar) and the rank-2-with-rank-2 case is the matrix
multiplication described above; both are instances of the one contraction rule,
which corresponds directly to the contraction operations already available in
MLIR under the tensor and linalg dialects.
Concatenation with || stacks arrays along their outermost axis: two
rank-k arrays with identical extents in every axis but the first join into a
rank-k array whose first extent is the sum of theirs, so two matrices
concatenate exactly when they have the same number of columns and the result
stacks their rows. Differing trailing extents are a SizeError and differing
ranks a TypeError (see Operations for the full rule, scalar
promotion, and the M || [r] row-append idiom).
integer[*][*] A = [[1, 2], [3, 4]];
integer[*][*] B = [[5, 6]];
/* A || B == [[1, 2], [3, 4], [5, 6]] */
The number of rows and columns in a matrix is given by the built-in
functions rows and columns; see Rows and Columns for
their full definition.
Matrix indexing is done similarly to array indexing. Because a matrix is an
array of arrays, indexing is composite: each subscript is applied, left to
right, to the value the subscripts before it produced. The [] operator is
left-associative, so M[i][j] groups as (M[i])[j]:
M[i][j] -> std_output;
The first subscript M[i] selects row i – a whole rank-1 array – and
the second subscript then indexes that row, so M[i][j] selects element
j of row i. When both indices are single integers, as here, the result
is the one element at that row and column:
integer[*][*] M = [[11, 12, 13], [21, 22, 23]];
/* M[1] == [11, 12, 13] (the whole first row) */
/* M[1][2] == 12 (its second element) */
As with arrays, out of bounds indexing on matrices must emit an
IndexError (see Errors) at compile time or
run time.
Each index position accepts the same forms as a 1-D array index (see
Array Slices): a single integer selects one element along the
current outermost axis and drops that axis, while a range written directly in an
index position selects a contiguous run along it and keeps it (a slice, with the
same bounds inclusive on both ends as for 1-D arrays). Because the
subscripts are applied one after another, each indexes the outermost remaining
axis of the value the previous subscripts produced – a matrix is peeled from
the outside in, exactly as in C. So M[i] selects a whole row (a rank-1
array), M[i][j] selects one element, M[1..2] selects a contiguous band
of rows (a rank-2 sub-matrix), and M[1..2][2] re-indexes that band to select
its second row. Higher-rank arrays generalize this to k subscripts.
integer[*][*] M = [[11, 12, 13], [21, 22, 23], [31, 32, 33]];
/* M[2] == [21, 22, 23] (a whole row) */
/* M[1..2] == [[11, 12, 13], [21, 22, 23]] (rows 1 and 2) */
/* M[1..2][2] == [21, 22, 23] (second row of those) */
Operator precedence and associativity are specified once, for all types, in the table of operator precedence.
8.10.4. Type Casting and Implicit Casts
To see the types that a matrix may be cast and/or implicitly cast to, see the sections on Type Casting and Implicit Casts respectively.