Function signature and call for a 2D matrix of m rows and n columns without comptime

I can pass a 2D matrix whose rows and columns are not known before hand like:

fn foo(comptime m: u16, comptime n: u16, matrix: [m][n]u16) void {
    // perform read-only operations on the matrix
}

But I was wondering whether its possible to avoid using comptime in the function signature. I tried a few things but struggling to get the syntax right.

If it is possible to do this, what would the function signature and call look like? And if its not possible, I’d like to understand why.


I’ve been documenting cases where comptime can be avoided:-

If the number of integers in a list is not known beforehand, the function signature and call could look like:

fn bar(slice: []const i16) void {
    for(slice, 0..) |value, i| {
        print("The value at index {d} is {d}\n", .{i, value});
    }
}

test bar {
    const sl1: []const i16 = &.{-6, 7, 1};
    const sl2 = &.{1, 4, -6, -2, 9};

    var arr1: [10]i16 = .{-10, -20, -30, -40, 50, -60, -70, -80, -90, 100};
    var arr2 = [_]i16{10, 20, 30, 40, 50, 60, 70, 80};

    bar(sl1);
    bar(sl2);
    bar(arr1[0..9]);
    bar(&arr2);
}

or it could also be:

fn baz(slice: []i16) void {
    for(slice, 0..) |value, i| {
        print("The value at index {d} is {d}\n", .{i, value});
    }
}

test baz {
    var arr1: [4]i16 = .{-6, 7, 1, -8};
    var arr2 = [_]i16{1, 4, -6, -2, 9, 10};

    baz(&arr1);
    baz(&arr2);
}

Another example is:

If the number of strings in a list is not known beforehand, the function signature and call could like:

fn foo(strings: []const []const u8) void {
    for (strings) |string| {
        print("{s}\t", .{string});
    }

    print("\n", .{});
}

fn bar(strings: [][]const u8) void {
    for (strings) |string| {
        print("{s}\t", .{string});
    }

    print("\n", .{});
}

test foo {
    {
        std.debug.print("array of strings\n", .{});

        const array_of_slices = [_][]const u8{
            "one",
            "two",
            "three",
            "four",
            "five",
        };

        foo(array_of_slices[0..]);
        foo(&array_of_slices);
    }

    {
        std.debug.print("slice of strings\n", .{});

        const slice_of_slices: []const []const u8 = &.{
            "one",
            "two",
            "three",
        };

        foo(slice_of_slices);
        foo(slice_of_slices[0..]);
    }
}

test bar {
    {
        std.debug.print("array of strings\n", .{});

        var array_of_slices = [_][]const u8{
            "one",
            "two",
            "three",
            "four",
        };

        bar(array_of_slices[0..]);
        bar(&array_of_slices);
    }
}

const std = @import("std");
const print = std.debug.print;

For the first example, you could use anytype, which would allow you to omit passing in the number of rows and columns, and use comptime reflection within the function body to determine those values. The use of anytype is usually not ideal, but this is a situation where it would be applicable. Note that although there is no “comptime” in the function, all types are comptime resolved, so passing in the wrong type will result in a compiler error.

pub fn foo(matrix: anytype) void {
    // This will fail if you pass in a type that is not a matrix
    const info = @typeInfo(@TypeOf(matrix));
    const m = info.array.len;
    const n = @typeInfo(info.array.child).array.len;
    // If you need the type
    const ChildType = @typeInfo(info.array.child).array.child;

    // Do stuff with matrix, m and n are the column/row lengths
    for (0..m) |column| {
        for (0..n) |row| {
            std.debug.print("MATRIX[{d}][{d}] = {d}\n", .{ column, row, matrix[column][row] });
        }
    }
}

pub fn main() !void {
    const identity = [4][4]u16{
        [4]u16{ 1, 0, 0, 0 },
        [4]u16{ 0, 1, 0, 0 },
        [4]u16{ 0, 0, 1, 0 },
        [4]u16{ 0, 0, 0, 1 },
    };

    foo(identity);
}

If you have the need for an array, and it is not possible to determine its length at comptime, then you need to use something else (i.e. a slice). Arrays are types with a fixed-size that is built into that type, and as mentioned above, types are comptime only.

1 Like

If you have the need for an array, and it is not possible to determine its length at comptime, then you need to use something else (i.e. a slice). Arrays are types with a fixed-size that is built into that type, and as mentioned above, types are comptime only.

Yes, array is not going to work because the number of rows and columns are not known. So it would have to be a slice.

I got this through trial and error:

fn foo(matrix: []const []const i16) void {
    const rows: usize, const cols: usize = .{matrix.len, matrix[0].len};

    for (0..rows) |row| {
        for (0..cols) |col| {
            print("The value at ({d}, {d}) is {d}\n", .{row, col, matrix[row][col]});
        }
    }
    print("\n", .{});
}

test foo {
    const two_by_two = &[_][]const i16{
        &[_]i16{ 1, 2 },
        &[_]i16{ 3, 4 },
    };

    const three_by_three: []const []const i16 = &.{
        &.{ 1, 2, 3 },
        &.{ 4, 5, 6 },
        &.{ 7, 8, 9 },
    };

    const two_by_four = [_][]const i16{
        &.{1, 2, 3, 4},
        &.{5, 6, 7, 8},
    };

    foo(two_by_two);
    foo(three_by_three);
    foo(&two_by_four);
    foo(two_by_four[0..]);
}

const std = @import("std");
const print = std.debug.print;

Is there another possible function signature that foo can have ?

1 Like
fn foo(matrix: []const i16, width: usize, height: usize) void {
    std.debug.assert(width * height == matrix.len);
    for (0..height) |y| {
        for (0..width) |x| {
            const idx = (y * width) + x;
            std.debug.print("{}, {}: {}", .{ x, y, matrix[idx] });
        }
    }
}

This assumes the matrix is stored as row major

For column major idx = (x * height) + y also swap the loops for efficiency.

2 Likes

Would be good to make a matrix type to bundle the data with the dimensions and an iterator to prevent using it wrong

1 Like

Yes for dynamic/runtime length multi dimensional data I would definitely prefer storing it as a flat slice with meta data for how to index into it, seems way better than having to create nested/intermediate slices for every dimension.

When the type that bundles the matrix data + width and height is written so that you don’t construct invalid instances, you could even use a multi-item ptr instead of a slice for the data and calculate the length via width * height, if/where it is needed. Then you can use a smaller type than usize for the meta data, that way the matrix is basically just a custom fat pointer:

const Matrix2D = struct {
     data: [*]i16,
     width: u32,
     height: u32, 

     // methods and conveniences ...
};
2 Likes

Also:

fn foo(matrix: [][]const i16) void {
    const rows: usize, const cols: usize = .{matrix.len, matrix[0].len};

    for (0..rows) |row| {
        for (0..cols) |col| {
            print("The value at ({d}, {d}) is {d}\n", .{row, col, matrix[row][col]});
        }
    }

    print("\n", .{});
}

test foo {

    var matrix = [_][]const i16{
        &[_]i16{ 1, 2, 3 },
        &[_]i16{ 4, 5, 6 },
    };

    foo(&matrix);
}

This is slightly more complex than what I was expecting - I’m in the process of documenting things related to structs so will come back to it soon :slight_smile:

[*]i16 isn’t that common. Assuming there’s a simple foo method in the Matrix2D struct, can you share how a function using data: [*]i16 will be called (from main or a unit test) ?

fn foo(matrix: [*]const i16, width: usize, height: usize) void {
    for (0..height) |y| {
        for (0..width) |x| {
            const idx = (y * width) + x;
            std.debug.print("{}, {}: {}", .{ x, y, matrix[idx] });
        }
    }
}

The difference between a [*] and [] is the latter contains length information, the former doesn’t, meaning [*] is generally less safe.
As @Sze said the length is already stored in width * height making the length property of a slice redundant.

Unlike slices, you can also do pointer maths with [*]

5 Likes