`const Self = @This();` in nested structs

I am confused a bit by compilation error. Here is the code:

const std = @import("std");

const S1 = struct {

    const Self = @This();

    const S2 = struct {
        const Self = @This();
        f2: u32 = 3,
        fn m2(self: *Self, s1: S1) u32 { // error here
            return self.f2 + s1.f1;
        }
    };

    f1: u32 = 4,

    fn m1(self: *Self) u32 {
        const s2 = S2{};
        return s2.m2(self);
    }
};

pub fn main() void {
    const s1 = S1{};
    std.debug.print("{}\n", .{s1.m1()});
}

Compiler says:

$ /opt/zig/zig build-exe ns.zig 
ns.zig:11:22: error: ambiguous reference
        fn m2(self: *Self, s1: S1) u32 { // error here
                     ^~~~
ns.zig:9:9: note: declared here
        const Self = @This();
        ^~~~~~~~~~~~~~~~~~~~
ns.zig:6:5: note: also declared here
    const Self = @This();
    ^~~~~~~~~~~~~~~~~~~~

Is it supposed to be like this?
I thought the first Self is for outer struct, and the second one is for nested one.

Of course one can do without @This():

const std = @import("std");

const S1 = struct {

    const S2 = struct {
        f2: u32 = 3,
        fn m2(self: *S2, s1: *S1) u32 {
            return self.f2 + s1.f1;
        }
    };

    f1: u32 = 4,

    fn m1(self: *S1) u32 {
        var s2 = S2{};
        return s2.m2(self);
    }
};

pub fn main() void {
    var s1 = S1{};
    std.debug.print("{}\n", .{s1.m1()});
}

But then there is no much profit in @This(), is there?

As I understand it, in Zig const Self = @This() is just a normal constant declaration where Self is the name of the constant. Self is used by convention, but it’s not special like in other languages, so you could use whatever identifier you want. In your code, you have Self declared twice in overlapping scopes so it is indeed ambiguous (although I would have expected a shadowing error instead, maybe someone with more knowledge can explain that.) Although not as convenient, you can use @This() directly too:

const std = @import("std");

const S1 = struct {

    const S2 = struct {
        f2: u32 = 3,

        fn m2(self: *@This(), s1: *S1) u32 {
            return self.f2 + s1.f1;
        }
    };

    f1: u32 = 4,

    fn m1(self: *@This()) u32 {
        var s2 = S2{};
        return s2.m2(self);
    }
};

pub fn main() void {
    var s1 = S1{};
    std.debug.print("{}\n", .{s1.m1()});
}

Note I had to change the s1 arg in method m2 to type *S1 to get the code to compile. Like in this case, if you’re not going to modify the arguments in the methods you could remove the pointers * to them, using constant arguments instead:

const std = @import("std");

const S1 = struct {

    const S2 = struct {
        f2: u32 = 3,

        fn m2(self: @This(), s1: S1) u32 {
            return self.f2 + s1.f1;
        }
    };

    f1: u32 = 4,

    fn m1(self: @This()) u32 {
        const s2 = S2{};
        return s2.m2(self);
    }
};

pub fn main() void {
    const s1 = S1{};
    std.debug.print("{}\n", .{s1.m1()});
}

My understanding is exactly the same - I just “mechanically” used Self = @This() and wrongly thought it might work since the scopes are different though nested.

Yes, this seems to be correct - if there was some function that takes both S1 and S2, using Self (or *Self) as a type for both arguments this would make the ambiguity more clear.

Why not using the name of a type directly instead (as in my second example)? What is the benefit of using @This?

When using @This() instead of the type name directly, then pretty much all of the benefits of Self apply, maybe sans looking subjectively as pretty.

But what are those benefits of aliasing type name with Self by means of @This()? Self is just a user defined named and, as @dude_the_builder noted, using this name for alias is a ‘common practice’ (however this common practice does not work with nested structs). Maybe it is a sort of homage to OOP languages?

I believe there are two main benefits to @This:

  1. If you later have to rename the struct, say from S1 to S2, you don’t have to go and replace all the references to S1 in your file because you aliased S1 to Self once and then used Self everywhere else.
  2. In Zig, an imported file is treated as a struct, allowing you to define your structs neatly as separate files. Within that file though, you won’t have a struct name given you don’t have an explicit const Name = struct {... statement, so in that case @This is your only alternative to reference the struct itself.
4 Likes

The first one is pretty obvious (but not very important imho), but the second one is really useful. Now I got it, thanks @dude_the_builder.

Nonetheless, in case of nested things we have to use different aliases names, like

pub const MessageDispatcher = struct {
    const Self = @This();
    pub const MessageQueue = struct {
        const MQ = @This();
2 Likes

But what are those benefits of aliasing type name with Self by means of @This()?

Zig generics are implemented by using an anonymous struct:

pub fn LinkedList(T: type) type {
    return struct {
        const Self = @This();
        next: *Self,
        payload: T,
        // ...
    };
}

Since you might have a linked list of u8 or a linked list of BigHonkinStructThatRequires1KbOfMemory, how do you declare a pointer to one? You can’t use LinkedList since that’s the function name, so … @This(). But typing that gets old right around the time you hit the ‘i’ on your keyboard. Thus, an alias is used: Self. Self aligns nicely with some other languages, and it somewhat intuitive when you see self: *Self as a parameter.

2 Likes

Okay, lets look at fn SinglyLinkedList from lib/std/linked_list. The returned structure contains nested structure (Node) with a couple of ‘methods’. And this nested struct does not uses Self = @This(), because it is impossible (you’ll get ambiguous reference).

pub fn insertAfter(node: *Node, new_node: *Node) void {
// Self = @This() idiom works only once, at the highest level.

As I said before, it is a sort of courtesy to OOP languages :slight_smile:

But, please, do not get me wrong - I am not criticizing (I am not languages/compilers designer at all), I am just learning Zig. As to Zig approach to generics: if they are impossible without @This compiler intrinsic - well, that’s nice, no problem.

One additional upside of using @This() directly (not assigning to Self) is avoiding the problems with nested definitions. That and less boilerplate, I guess.

Sorry I’m having trouble following so what is the right solution
self or @this

Self and @This are not mutually exclusive. You may assign the result if the latter to something (not necessarily to Self, it can be any name), or you may not… depends on a situation. Self is just “traditional” alias for the name of a structure, inside which it is used.

1 Like

There is no right solution here, it is (mostly) the matter of personal preferences or coding rules, I think. Look at this:

const SomeThing = struct {
    const Self = @This();
    const ST = @This();
    fn f1(self: Self) void {...
    fn f2(self: ST) void {...
    fn f3(me: @This()) void {...
    fn f4(st: ST) void {...
    fn f5(arg: SomeThing) void {...
    // and so on... all these are absolutely the same
}
2 Likes

You can even do… well… This:

onst S1 = struct {
   a: usize = 0,

   const This = @This();

   fn m1(this: This) void {
      std.log.info("{}", .{this.a});
   }
};

:smile:

2 Likes

As far as I could understand from the discussion, the only case when we must use @This() is implementing generic data types. Is this true? Are there any other cases/situations where @This() is unavoidable?

if you nest a struct definition in a fn body or a test, you can’t use the struct identifier directly, so you have to use @This() (or a const alias to @This())

Where exactly the struct identifier can not be used in this case?..

const std = @import("std");

fn func(c: i32) void {
    const SomeThing = struct {
        a: i32 = 1,
        b: i32 = 2,
    };
    const thing = SomeThing{}; // <<<<<<<<<<
    const d = thing.a + thing.b + c;
    std.debug.print("{}\n", .{d});
}

pub fn main() void {
    func(3);
}

Everything is ok with this example.

The restriction is that you can’t refer to the struct name inside the struct. No matter what you can refer to it outside by name.

    fn f() void {
        const U = struct {
            fn f(_: *U) void {} // <<< fails here because U is not defined
        };
        _ = U;
    }
1 Like

And as I guessed just now, generics is a subcase of this, right?

seems like it could be considered a subcase for us users, not sure if it is actually the same case for the compiler internally