How do I pass a multidimensional slice to a generic function?

Hi,

How can I pass to a generic function a multidemnsional array? Thanks in advance. Basically I don’t want to pass an array of specific size but of various.

So far I am stuck with this code. And I saw some answers but didn’t quite understand any of them.

test "Case 1" {
    const accounts = [_][_]i8{
        [_]i8{1, 2},
        [_]i8{3, 4},
    };
    const answer = 4;

    expect(max(&accounts) == answer);
}

fn max(a: *const [][]i8) i16 {
    var max: i16 = 0;
    for (rows) |columns| {
        for (columns) |cell| {
          if (max < cell) {
              max = cell;
          }
        }
    }
    return max;
}

You need to understand the differance between a slice and an array. Unlike in C where arrays are pointers to multiple data, in Zig arrays are actual data, passed around by copy. Slices are more the equivalent of C’s arrays, exept that they both hold a pointer and their lenght.

In your code you declare an array of arrays, but your function expects a slice of slices. Even if you referenced an array, reference that can coerce to a slice, it’d be a slice of arrays, not a slice of slices.

6 Likes

The following code has minimal changes to pass the test.

const std = @import("std");

test "Case 1" {
    // cannot infer both sizes
    const accounts = [_][2]i8{
        [_]i8{ 1, 2 },
        [_]i8{ 3, 4 },
    };
    const answer = 4;

    // try is required because expect might fail
    try std.testing.expect(max(&accounts) == answer);
}

// you must declare the full size of the array
fn max(a: *const [2][2]i8) i16 {
    // cannot have a variable named max here, because it shadows max function
    var _max: i16 = 0;
    for (a) |columns| {
        for (columns) |cell| {
            if (_max < cell) {
                _max = cell;
            }
        }
    }
    return _max;
}

Running the test can help you understand how to proceed.
The reported errors was:

unable to infer array size
    const accounts = [_][_]i8{
                         ^

error: local variable shadows declaration of 'max'
    var max: i16 = 0;
        ^~~
note: declared here
fn max(a: *const [][]i8) i16 {
^~

expected type '*const [][]i8', found '*const [2][2]i8'
    std.testing.expect(max(&accounts) == answer);
                           ^~~~~~~~~
note: pointer type child '[2]i8' cannot cast into pointer type child '[][]i8'
note: parameter type declared here
fn max(a: *const [][]i8) i16 {
          ^~~~~~~~~~~~~

error: error union is ignored
    std.testing.expect(max(&accounts) == answer);
    ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~
note: consider using 'try', 'catch', or 'if'
2 Likes

Here’s a slightly different way, if you want to pass slices of slices. It can be tough getting the initial variable assigned to the constant. You want to specify the type of the variable, and anything that points at constant data needs the “const” specifier.

const std = @import("std");

test "Case 1" {
    const accounts: []const []const i8 = &[_][]const i8{
        &[_]i8{ 1, 2 },
        &[_]i8{ 3, 4 },
    };
    const answer = 4;

    try std.testing.expect(max(accounts) == answer);
}

fn max(a: []const []const i8) i8 {
    var temp: i8 = 0;
    for (a) |column| {
        for (column) |cell| {
            if (temp < cell) {
                temp = cell;
            }
        }
    }
    return temp;
}
3 Likes

Shouldn’t I be able to create a slice of slices that just references the same memory that aray of arrays does?

Yep - that’s what I’m doing with the const accounts: line. The tricky part is getting the const-ness right.

Thank you for the good points.

Also I had to clarify the post because I want to be able to pass matrixes of various dimensions. So I would like the function to be generic in terms of lengths.

Consider the simpler approach to declare the two dimensional matrix as single dimensional array sized width*height and access the elements as [i+j*width] instead of [i][j].
When using slices, a benefit is allocating the contents once instead of 1+height times.

Arrays have compile time knowledge of their size and limit you because you must always know their size.
Slices have runtime knowledge of their size and is the only way when the size is not known until runtime.

2 Likes

You can also simplify @david_vanderson’s solution. The compiler is able to infer the array types from the result type, so you can use . instead of explicitly naming the type in every line:

    const accounts: []const []const i8 = &.{
        &.{ 1, 2 },
        &.{ 3, 4 },
    };

But keep in mind that with slices this will be stored quite inefficiently. Here is roughly how this would look in memory:

accounts = .{.ptr = 0xabcd, .len = 2}
0xabcd = .{.ptr = 0xbcde, .len = 2, .ptr = 0xcdef, .len = 2}
0xbcde = .{1, 2}
0xcdef = .{3, 4}

That’s why in practice I would recommend to make a struct that makes sure everything is flat in one array. This is what I use when the array length is runtime-known:

pub fn Array2D(comptime T: type) type {
	return struct {
		const Self = @This();
		mem: []T,
		width: u32,
		height: u32,

		pub fn init(allocator: Allocator, width: u32, height: u32) !Self {
			return .{
				.mem = try allocator.alloc(T, width*height),
				.width = width,
				.height = height,
			};
		}

		pub fn deinit(self: Self, allocator: Allocator) void {
			allocator.free(self.mem);
		}

		pub fn get(self: Self, x: usize, y: usize) T {
			std.debug.assert(x < self.width and y < self.height);
			return self.mem[x*self.height + y];
		}

		pub fn getRow(self: Self, x: usize) []T {
			std.debug.assert(x < self.width);
			return self.mem[x*self.height..][0..self.height];
		}

		pub fn set(self: Self, x: usize, y: usize, t: T) void {
			std.debug.assert(x < self.width and y < self.height);
			self.mem[x*self.height + y] = t;
		}

		pub fn ptr(self: Self, x: usize, y: usize) *T {
			std.debug.assert(x < self.width and y < self.height);
			return &self.mem[x*self.height + y];
		}
	};
}

The memory layout of this is much better:

accounts = .{.mem.ptr = 0xabcd, .mem.len = 4, width = 2, .height = 2}
0xabcd = .{1, 2, 3, 4}
3 Likes

Gotcha, I see that I goofed up with consts.

As I understood, you are defining accounts as slices from the begging by creating an array and then getting a pointer to it with &. However, I am wondering, is possible to keep the original declaration as-is and get a slice of slices anyway?

    const accounts = [_][_]i8{
        [_]i8{1, 2},
        [_]i8{3, 4},
    };

It’s very tempting to want to write code like that, and I’ve tried many times. The important thing is you can’t assign constant data to non const variables.

@IntegratedQuantum has the best example of how to initialize the accounts variable, but it still has to be const, because it’s pointing to read-only parts of the compiled output program.

That example is giving a slice of slices. In what way is that not working for you?

Sounds like Dok8tavo is right and I am just spoiled by other languages the allocate runtime know data implicitely.

I’m glad you brought memory represention. I am quite a novice here. Could you tell me how I could get something like this in memory representation?

Thank you for the full example.

Here’s an example of how to allocate on the heap and copy the test data, so you have mutable slices. It could be made better by:

  • freeing the memory
  • allocating it with a different allocator maybe
  • combining the cols/rows like @dimdin suggested
const std = @import("std");

var gpa_instance = std.heap.GeneralPurposeAllocator(.{}){};
const gpa = gpa_instance.allocator();

test "Case 1" {
    const data1 = [2][2]i8{
        [_]i8{ 1, 2 },
        [_]i8{ 3, 4 },
    };
    const slice1: [][]i8 = try gpa.alloc([]i8, data1.len);
    for (0..data1.len) |i| {
        slice1[i] = try gpa.alloc(i8, data1[i].len);
        @memcpy(slice1[i], &data1[i]);
    }

    const answer = 4;

    try std.testing.expect(max(slice1) == answer);
}

fn max(a: [][]i8) i8 {
    var temp: i8 = 0;
    for (a) |column| {
        for (column) |cell| {
            if (temp < cell) {
                temp = cell;
            }
        }
    }
    return temp;
}

This a good point, but it is a seprate topic from the slice of slices. I didn’t precicely want it to be a constant. I think at some point ZLS promted me to make it and I have just left it that way before posting. I get your point though I defintively wouldn’t have caught that myself. Not yet.

To be precise I am just trying to better understand the rules imposed by Zig. On top of that my main language is Python. :slight_smile:

So I get the feeling that it should be possible to have an array of arrays declared and then get slice of slices from it, but as many people indicated, this might not in fact the optimal thing to do with Zig anyway.

Well you can just use the Array2D struct I provided. You just need to setup an allocator (inside of tests you can of course use the testing allocator):

    const accounts = [_][_]i8{
        [_]i8{1, 2},
        [_]i8{3, 4},
    };
    const arr2d = Array2D(i8).init(std.testing.allocator, accounts.len, accounts[0].len);
    defer arr2d.deinit(std.testing.allocator);
    for(0..accounts.len) |x| {
        for(0..accounts[0].len) |y| {
            arr2d.set(x, y, accounts[x][y]);
        }
    }
    const answer = 4;

    expect(max(&accounts) == answer);

Now this may look a bit cumbersome, but in this case you can actually just skip the allocation entierely by storing the matrix as a 1 dimensional array on the stack directly:

    var accounts = [_]i8{
        1, 2,
        3, 4,
    };
    const arr2d: Array2D(i8) = .{.mem = &accounts, .width = 2, .height = 2};
    
    const answer = 4;
    expect(max(&accounts) == answer);

Either way your max function would then just look something like this:

fn max(a: Array2D(i8)) i16 {
    var max: i16 = 0;
    for (0..a.width) |x| {
        for (a.getRow(x)) |cell| {
          if (max < cell) {
              max = cell;
          }
        }
    }
    return max;
}
2 Likes