Help with syntax for Multidimensional Array of Structs Initializer

I’m struggling with the syntax for how to initialize my array for the test and if there is a more terse way to initialize the md array with all the structs.

pub const CellState = struct {
    is_mine: bool = false,
    is_covered: bool = true,
    marking: Marking = Marking.None,
    mine_neighbors: u4 = 0,
};

fn is_board_won(board: [][]CellState) bool {
    for (board) |column| {
        for (column) |cell| {
            if (cell.is_covered and !cell.is_mine)
                return false;
        }
    }

    return true;
}
test "is_board_won_true_all_cells_uncovered" {
    var board = [3][3]CellState{
        [3]CellState{
            .{ .is_covered = false },
            .{ .is_covered = false },
            .{ .is_covered = false },
        },
        [3]CellState{
            .{ .is_covered = false },
            .{ .is_covered = false },
            .{ .is_covered = false },
        },
        [3]CellState{
            .{ .is_covered = false },
            .{ .is_covered = false },
            .{ .is_covered = false },
        },
    };

    try std.testing.expectEqual(is_board_won(&board), true);
}

Error output is

matt@matt-dev-one:~/code/minesweeper-zig$ zig test src/minesweeper/game.zig
src/minesweeper/game.zig:441:46: error: expected type '[][]game.CellState', found '*[3][3]game.CellState'
    try std.testing.expectEqual(is_board_won(&board), true);
                                             ^~~~~~
src/minesweeper/game.zig:441:46: note: pointer type child '[3]game.CellState' cannot cast into pointer type child '[]game.CellState'
src/minesweeper/game.zig:412:24: note: parameter type declared here
fn is_board_won(board: [][]CellState) bool {
                       ^~~~~~~~~~~~~

The issue I see is that the function expects a non-const slice of non-const slices, which isn’t a problem in and of itself but it doesn’t allow you to initialize your inneer slices with literals because those are const. If you really needed the inner slices to be non-const, you would have to create non-const arrays for each and then slice those arrays to put them in your outer array. On the other hand, if you really don’t need them to be const (at least in is_board_won you don’t mutate them,) you can change your param type to [][]const CellState or even []const []const CellState if you don’t need to mutate the outer slice either. With that change, you can initialize with:

fn is_board_won(board: []const []const CellState) bool {
// or
fn is_board_won(board: [][]const CellState) bool {
// ...

    var board = [3][]const CellState{
        &.{
            .{ .is_covered = false },
            .{ .is_covered = false },
            .{ .is_covered = false },
        },
        // ...
4 Likes

Hello @programatt Welcome to ziggit :slight_smile:

1st way:

fn is_board_won(board: *const [3][3]CellState) bool {

2nd way:

fn is_board_won(board: []const []const CellState) bool {
...
test "is_board_won_true_all_cells_uncovered" {
    const board = [3][3]CellState{
        [3]CellState{
            .{ .is_covered = false },
            .{ .is_covered = false },
            .{ .is_covered = false },
        },
        [3]CellState{
            .{ .is_covered = false },
            .{ .is_covered = false },
            .{ .is_covered = false },
        },
        [3]CellState{
            .{ .is_covered = false },
            .{ .is_covered = false },
            .{ .is_covered = false },
        },
    };
    var outer: [3][]const CellState = undefined;
    for (0..3) |i| {
        const inner: []const CellState = &board[i];
        outer[i] = inner;
    }
    const slice: []const []const CellState = &outer;
    try std.testing.expectEqual(is_board_won(slice), true);
}
3 Likes

Another thing you may want to consider, is turning your n-dimensional array into a 1 dimensional array that you index into, with a calculated index.

If you got x and y you can use x + y * columns to index into the array.
If you got an index i you can use i % columns to get x and i / columns to get y.

You also can swap this around and use x * rows + y instead (to have all values of a column contiguous).
One of the benefits of this is that you don’t have to create the intermediate n-1 dimensional slices, for the lower dimensions and instead everything is kept in one flat array. (This is especially useful for many dimensional arrays)

I think languages like Julia and Wolfram (probably others) often put these x y z … indices into one struct, that then handles the conversion between the n-dimensional coordinate to the resulting index (this index is calculate differently, depending on the order of the dimensions (do you keep rows contiguous, or columns etc.))

The interesting part about this, is that this enables you to easily swap how the coordinate vector gets transformed into the flattened index, allowing you to experiment with different traversal orders and how the memory is laid out, without having to rewrite how your code is written.

This works because you basically change your multiple indexing operations which have to be done in a specific order, into one index operation that calculates the index in some specific order.

Additionally when traversing the whole multi-dimensional array you sometimes can pick the layout in a way so that it coincides with just traversing the flat array lineraly, which is the best case scenario, instead of having to hop around by multiples of rows / columns. This can be beneficial in avoiding cache misses.

2 Likes

Interesting. So for completeness the code that is creating the [][]CellState struct in the main application is using an allocator so it’s mutable there which I think is necessary because the values are being mutated in the rest of the code. So the reason why modifying the signature to add the const keywords doesn’t break that is that it’s ok to pass a mutable object to a function that treats it as const but not vice versa?

1 Like

Yes, going from non-const to const is OK because it’s always safe, being more restrictive. Going from const to non-const is less-restrictive and thus not allowed (unless you use a const cast, but that’s usually a special case needed for interop with some external library.)