Types in zig

I am currently experimenting with types in Zig and I came across something that I couldnt make sence of.

The Zig way of doing generics is something like this:

pub const Bra(T: type) type {
   return sturct { ... };
}

And I worked a bit with this ziguanic way of doing it and I like it very much. But after a while I was curious for a deeper understanding and I also had the problem what to do when I want a type to be dependent on 2 input variables and I played around and came up with this:

fn F(T: type, comptime i: usize) type {
    return struct {
        const Self = @This();
        const a: usize = i;
        const b: usize = 1337;

        fn init() Self {
            return Self{};
        }

        fn g(self: Self) usize {
            _ = self;
            return a;
        }

        fn bra(self: Self, o: Self, t: T) void {
            std.debug.print("self: {} other: {} , {}\n", .{ self.g(), o.g(), t });
        }
    };
}

At the beginning I thought that maybe the type of this would depend on the const elements inside the struct (because you always see this const Self = @This();), but after some print type information I was proven wrong.

pub fn main() void {
    const f1 = f(u32, 1);
    const f2 = f(u32, 2);

    const l1 = f1.init();
    const l2 = f2.init();
    l1.a(l2, 10)
}

After I tried this I got this compiler error:

src/main.zig:31:12: error: expected type 'main.F(u32,1)', found 'main.F(u32,2)'
    l1.bra(l2, 10);
           ^~
src/main.zig:5:12: note: struct declared here (2 times)
    return struct {
           ^~~~~~
src/main.zig:19:31: note: parameter type declared here
        fn bra(self: Self, o: Self, t: T) void 

So currently my assumption is that a type depends on the function call? Is there another way to declare a type? Did I miss something?

Thanks for your help!

Side questions: Is a struct {} of type type?

1 Like

If I remember correctly, Types that are created at comptime are unique, even if they only vary very slightly like yours, so from your example, you are creating 2 unique type, and then you are trying to pass to the parameter self, another type than the one you are supposed to pass. Of course this is my understanding, I might be wrong here, But I think this is what’s happening. Also yes struct {} is invoking a type.

2 Likes

Edit: While I was writing, I guess I answered my own questions, which might be the reason why there is not really a question in this statement.

Yes. That it fails was also my intention. Currently I am wondering what is the way to define a type. As in my example I have a function that takes to compile time parameters and returns a type, yet I return a struct that should describe the type, but it seems the type is described by the function call with the 2 compile time parameters.

What I am trying to understand is what is the underlining concept of types in zig? At first I thought it was a struct because we also have something like:

pub const Foo = struct {
   ...
};

Where here Foo describes the struct and where I can reference it later to say this is the struct I want to work with. In C, this would be something like this:

struct Bra {
   ...
};

or

typedef struct {
   ...
} Bra ;

In the end a struct is just some packing of data, and in zig we can associate some function to it, so we have a better programming experience.

But if we introduce types (or kinda generics) to this, what a struct is changes in some sense.

When I change this:

fn F(T: type, comptime i: usize) type {
    return struct {
        const Self = @This();
        const a: usize = i;
        const b: usize = 1337;

        fn init() Self {
            return Self{};
        }

        fn g(self: Self) usize {
            _ = self;
            return a;
        }

        fn bra(self: Self, o: Self, t: T) void {
            std.debug.print("self: {} other: {} , {}\n", .{ self.g(), o.g(), t });
        }
    };
}

to:

fn F(T: type, comptime i: usize) type {
    _ = i;
    return struct {
        const Self = @This();
        const b: usize = 1337;

        fn init() Self {
            return Self{};
        }

        fn g(self: Self) usize {
            _ = self;
            return a;
        }

        fn bra(self: Self, o: Self, t: T) void {
            std.debug.print("self: {} other: {} , {}\n", .{ self.g(), o.g(), t });
        }
    };
}

My example works again. So I assume that types depend on the const values of a struct and the fields?

I tried also something else and it seems like I can you this function as my own type:

fn A(T: type) type {
    return struct {
        const Self = @This();

        b: F(T, 1),

        fn init() !Self {
            return Self{ .b = F(T, 1).init() };
        }
    };
}

I think this post might help you a bit, understanding comptime can help understanding how the type system work

implementation of comptime

1 Like

There is also this one link I think this might help you as well :slight_smile:
Types and Zig

2 Likes

Thanks! I take a look.

Because i isn’t used within the struct, the struct type doesn’t depend on i which is why only T contributes to what parameter combination results in unique types/versions of this generic struct.

The post @pierrelgol has linked to, explains that the external dependencies are used to determine whether two types are the same. So the unused i has no effect, but if you use it in some way within the struct, then the two F calls produce different types again.

1 Like