Grouping methods and outsourcing them to another file

Hi,

I want to define a struct with some methods. However I would like to group the methods and put them into a separate file. For example have different algorithms to solve the same problem, but have them available.
My goal is to be able to call them like

mymatrix_a.matmul.naive(mymatrix_b);
mymatrix_a.matmul.strassen(mymatrix_b);
...

How would I go about it?
My idea was to write a file that simply contains the functions:

/// matmul.zig
pub fn strassen(...) {
    ...
}
pub fn naive(...) {
    ...
}

Then I define the main struct in another file like:

/// mystruct.zig
const MyStruct = struct {
    ...,
    const Self = @This();
    const matmul = @import("matmul.zig");
    pub fn init() Self {
        return Self{
            ...
        };
    }
    pub fn deinit(self: *Self) void {
        _ = self;
    }
};

Apart from the fact that I need to figure out how I would access the fields from mystruct in the external functions, i have the problem that the compiler complains the field matmul would not exist if I try to access it.
I would appreciate your help. Thanks

I think you would need to define the methods physically inside the struct definition:

const MyStruct = {
   const algos = @import("matmul-algos.zig");
   pub const strassen = algos.strassen;
   ...
}

The methods should have no issues accessing your instance fields. The struct name needs to be in scope, which can just be @import’ed.
Question: If you want them in a separate file, maybe you also don’t need them to be methods? Just algos.strassen(matA, matB) should work, or?

Thank you for your reply.
If I define the methods as you suggested, the grouping by algorithm type is gone, as they are top level methods in MyStruct.
Regarding the question if they have to be methods. No, of course not, but it would be much nicer. I actually have a generic Struct that takes some comptime types, and thus the methods should be tuned to the same comptime types.
If I define them as external functions I would need to pass the type with every call, which I find annoying.

Good news - the method grouping you describe is in fact a Zig idiom! The methods gain access to the data of the containing struct using a mechanism called @fieldParentPtr. If you use the search function on this forum you’ll find lots of examples. I’ll post something concrete later when I’m back at computer, unless someone else does first.

As for your inability to import, it may be because you are not using the build system. It’s important to learn, but one thing at a time - I’d advise keeping it all in a single file for now.

1 Like

That is because it isn’t a field, you define it as a declaration on the MyStruct type, if you want it to be a field you need to use:

const MyStruct = struct {
    ...
    matmul: @import("matmul.zig"),
    ...
};

Only member functions and fields can be used with dot access on an instance, if it is some non-member function declaration you can only access it on the type directly, not the instance.

And then use @fieldParentPtr like @affine-root-system mentioned, in the actual naive function.

One problem is that @import("matmul.zig") has no idea what the parent type is and it is unlikely that you want to hardcode that to only a single MyStruct, so what you actually need is to pass @This() to what ends up as the matmul field, this means you no longer can use @import("matmul.zig") directly (because you can’t pass parameters to an import).

Instead you need to use something like this (possibly adding the explicit field name for more flexibility, or even other options):

const MyStruct = struct {
    ...
    matmul: @import("matmul.zig").Gen(@This(), "matmul"),
    ...
};
2 Likes

Thank you, this was exactly what I needed. Just for future reference, here is how I constructed it:

///struct.zig
const std = @import("std");
const Algos = @import("algos.zig").Algos;

pub fn MyStruct(comptime T: type) type {
    return struct {
        load: T,
        algos: Algos(T),

        const Self = @This();
        pub fn init(load: T) Self {
            return Self{
                .load = load,
                .algos = .{},
            };
        }
        pub fn deinit(self: *Self) void {
            _ = self;
        }
    };
}
///algos.zig
const std = @import("std");
const MyStruct = @import("struct.zig").MyStruct;

pub fn Algos(comptime T: type) type {
    return struct {
        const Self = @This();
        pub fn variant1(self: *Self) void {
            const parent: *MyStruct(T) = @alignCast(@fieldParentPtr("algos", self));
            std.debug.print("running variant1 with val {any}\n", .{parent.load});
        }
        pub fn variant2(self: *Self) void {
            const parent: *MyStruct(T) = @alignCast(@fieldParentPtr("algos", self));
            std.debug.print("running variant2 with val {any}\n", .{parent.load});
        }
    };
}
///main.zig
const std = @import("std");
const MyStruct = @import("struct.zig").MyStruct;
pub fn main() !void {
    var mys = MyStruct(f64).init(3.14);
    defer mys.deinit();
    mys.algos.variant1();
    mys.algos.variant2();
}

So everything works as expected.

3 Likes