Continuing the discussion from Arrays vs vectors:
I recently came across the curious case of wanting to apply a newtype pattern to
a slice.
So let’s create a type specifically for lower triangular matrices. Let’s also assume it shall work without allocation, using arrays as backing storage and slices referring to that backing memory.
Given an element type T
, we can have two different slices of type T
:
[]T
[]const T
We need to use the latter when we have some const
memory where our data comes from, but the former when we want to support mutation.
Now instead of creating two types (boilerplate ), it would be nice to create a single type constructor:
pub fn Triangular(comptime Linear: type) type {
return struct {
linear: Linear,
}
}
This way, given elements of some type, say i32
, we can create two distinct types:
Triangular([]i32)
, which is a mutable lower triangular matrixTriangular([]const i32)
, which is a constant lower triangular matrix
We know from slices that a []const i32
can coerce to a []i32
(see also Type Coercion: Stricter Qualification in the Zig documentation):
Values which have the same representation at runtime can be cast to increase the strictness of the qualifiers, no matter how nested the qualifiers are:
const
- non-const to const is allowed
Nowever, this “nesting” doesn’t seem to apply to struct fields, as demonstrated below:
const std = @import("std");
const assert = std.debug.assert;
const print = std.debug.print;
fn triNum(x: usize) usize {
return x * (x + 1) / 2;
}
fn triIdx(row: usize, col: usize) usize {
assert(col <= row);
return triNum(row) + col;
}
pub fn Triangular(comptime Linear: type) type {
return struct {
const Element = std.meta.Elem(Linear);
linear: Linear,
pub fn assertDim(self: @This(), dim: usize) void {
assert(self.linear.len == triNum(dim));
}
pub fn get(self: @This(), row: usize, col: usize) Element {
return self.linear[triIdx(row, col)];
}
pub fn set(self: @This(), row: usize, col: usize, value: Element) void {
self.linear[triIdx(row, col)] = value;
}
// Do we need the following function? Unfortunately, we do!
pub fn asConst(self: @This()) Triangular([]const Element) {
return Triangular([]const Element){ .linear = self.linear };
}
};
}
pub fn printTriangularInt(dim: usize, matrix: Triangular([]const i32)) void {
matrix.assertDim(dim);
for (0..dim) |row| {
for (0..row + 1) |col| {
if (col > 0) print(" ", .{});
print("{d:3}", .{matrix.get(row, col)});
}
print("\n", .{});
}
}
pub fn main() void {
// Let's say we have some constant data:
const const_data: [6]i32 = [_]i32{ 1, 2, 3, 4, 5, 6 };
// Note that we then need a `[]const` slice here:
const triangular1 = Triangular([]const i32){ .linear = &const_data };
// This works fine:
printTriangularInt(3, triangular1);
// Now let's have some non-const backing memory, allowing mutation:
var buffer: [6]i32 = undefined;
const triangular2 = Triangular([]i32){ .linear = &buffer };
for (0..3) |row| {
const i: i32 = @intCast(row);
for (0..row + 1) |col| {
const j: i32 = @intCast(col);
triangular2.set(row, col, 10 * (i + 1) + (j + 1));
}
}
// Now this doesn't work:
//printTriangularInt(3, triangular2);
// Instead we need:
printTriangularInt(3, triangular2.asConst());
}
Question: Should Triangular([]const i32)
coerce to Triangular([]i32)
automatically, without needing the asConst
function?