Zig NEWS

Cover image for Don't `Self` Simple Structs!
Loris Cro
Loris Cro

Posted on

Don't `Self` Simple Structs!

EDIT: as some people pointed out in the comments, "concrete" (as in non-generic) instead of "simple" would have conveyed the intended meaning more effectively.

I've seen a few examples of people writing this kind of code:

const Person = struct {
   name: []const u8,

   const Self = @This(); 

   pub fn talk(self: Self) void {
      std.debug.print("Hi, I'm {s}\n", .{self.name});
   }
}
Enter fullscreen mode Exit fullscreen mode

Don't!

There's no need to declare a Self type alias when you're adding methods to a simple struct!

Just use the struct name:

const Person = struct {
    name: []const u8,

   pub fn talk(self: Person) void {
      std.debug.print("Hi, I'm {s}\n", .{self.name});
   }
}
Enter fullscreen mode Exit fullscreen mode

When is Self needed?

Self is just a convenience alias that people like to create. It's almost never necessary, so let's talk fist about when @This() is necessary.

@This() is needed when you are implementing a generic structure. In this case you are inside a function and are defining a struct type that you plan to return. That struct will usually not have a name, so @This() lets you refer to it even in the absence of a name:

pub fn Box(comptime T: type) type {
   return struct {
      data: T,
      mutex: std.Thread.Mutex,

      const Self = @This(); 
      pub fn unlock(self: Self) T {
         // ...
      }
   };
}
Enter fullscreen mode Exit fullscreen mode

Just to reiterate how Self is just a convenience alias, this code is the exact same as above:

pub fn Box(comptime T: type) type {
   return struct {
      data: T,
      mutex: std.Thread.Mutex,

      pub fn unlock(self: @This()) T {
         // ...
      }
   };
}
Enter fullscreen mode Exit fullscreen mode

So Self is never necessary?

Sometimes it is necessary to create an alias to @This(), however it's a fairly rare need that stems from having multiple nested anonymous container type definitions:

pub fn Box(comptime T: type) type {
   return struct {
      data: T,
      mutex: std.Thread.Mutex,
      hidden_compartment: InnerBox(@sizeOf(T)),

      const OuterBox = @This(); // doesn't have to be called Self

      fn InnerBox (comptime Size: usize) type {
         return struct {
            secret_bytes: [Size]u8,
            magic_link: *OuterBox; // using @This() wouldn't work
         };
      }

   };
}
Enter fullscreen mode Exit fullscreen mode

In this example you can see how the InnerBox declaration needs to refer to the outer box when defining magic_link. In this case @This() would not have worked since, when used in that context, it would refer to the innermost struct type, not the outer.

As you can see by the convoluted nature of this last example, it's not something you normally need to do, and you also have plenty of escape hatches to avoid nesting type definitions this way even when your types get somewhat complicated.

Last thing but not least, it's also not mandatory to name the "self" parameter self:

pub fn talk(p: Person) void {
   std.debug.print("Hi, I'm {s}\n", .{p.name});
}
Enter fullscreen mode Exit fullscreen mode

That said, self (parameter name) is useful for easily recognizing who's the "subject" of a given method, and Self is useful for quickly recognizing that we're looking at a generic type implementation...

...as long as we make sure to not use it for simple structs.

Top comments (21)

Collapse
 
lewisgaul profile image
Lewis Gaul

I'd be interested in your thoughts on this counterargument.

Focusing locally on a struct method, is there a bug here?

...
    pub fn do_thing(self: Bar) void {
        ...
    }
...
Enter fullscreen mode Exit fullscreen mode

Well, maybe, this shows how there could've been a copy-paste error, which I believe would compile:

const Bar = struct {
    ...
    pub fn do_thing(self: Bar) void {
        ...
    }
};
const Baz = struct {
    ...
    pub fn do_thing(self: Bar) void {
        ...
    }
};
Enter fullscreen mode Exit fullscreen mode

If @This() were used instead of the struct name then the original snippet couldn't have this specific bug - it precisely states the intent within that local code snippet.

Using @This() would also mean one less place you'd need to update if you ever changed the name of the struct (could argue the naming of the struct shouldn't leak into the implementation, isn't that a big part of the reason for @This() to exist?).

That said, I certainly wouldn't say it's wrong to use the struct name!

Collapse
 
kristoff profile image
Loris Cro • Edited

I think the do_thing with the wrong self type bug is not really going to be much of a thing because you will get a compile error as soon as you try to use it with normal dot notation:

const a: Bar = .{};
a.do_thing(); // compile error
Enter fullscreen mode Exit fullscreen mode

Using @This() would also mean one less place you'd need to update if you ever changed the name of the struct

True, on the flip-side if you're scrolling through a big file, it will take you more effort to figure out what @This() is when jumping around. Personally I prefer prioritizing the reader in this case over facilitating tool-less refactoring (I recently noticed that ZLS is already able to rename symbols)

I think all in all these are minor things that are a matter of taste to a good degree, my real beef is with people not realizing at all that you can use the original struct name, and thinking that they are forced to write extra boilerplate when in fact they don't have to.

Collapse
 
hachanuy profile image
Uy

I feel like you writing an absolute article (not really but the title certainly reads like that) because there are people reading Zig code and forming false assumptions about it based on the boilerplate

const Self = @this;
Enter fullscreen mode Exit fullscreen mode

But I think that creates the adverse effect of avoiding that unnecessarily, since you say it should be avoided for simple structs and simple is relative. I think it's better to say to not use it for concrete structs.

One point you didn't mention is using the actual name makes the code base easier to grep.

Thread Thread
 
kristoff profile image
Loris Cro

I think it's better to say to not use it for concrete structs.

Fair, I used "simple" in the title because it sounds better than "non-generic", but in the article you can see how the real distinction is non-generic vs generic. I think "concrete" also sounds nice, but I'm not sure how widespread is it's use to mean "non-generic". Had I thought about that term, I would probably have used it in the title anyway.

Oh well :^)

Collapse
 
leroycep profile image
LeRoyce Pearson

There is one other place where it makes sense to use @This(): when you are using a file as a struct. Then the only ways to reference the struct are @This() or @import().

Collapse
 
kristoff profile image
Loris Cro

True!

Collapse
 
nairou profile image
Nairou

I use files as structs a lot, so I was confused by this article until your comment. Thanks!

Collapse
 
jibal profile image
jibal • Edited

There's a big difference between "X isn't necessary" and "Don't do X!"

I habitually use Self because a) I often change the names of my types, b) it makes it easier to copy/paste, c) there are inherent upsides to consistency.

Self is useful for quickly recognizing that we're looking at a generic type implementation.

Nope, wrong ... that is an erroneous mental model. Generic types are identified by the fact that they have comptime T: type (or other single capital letter) parameters. (Or other comptime parameters ... e.g., there are types that are generic over numeric values like the size of a buffer, not just over types. But in any case, Self has nothing to do with it. "@This() is needed when you are implementing a generic structure." -- nope, it's needed when you have an unnamed ("anonymous") struct.)

...as long as we make sure to not use it for simple structs.

There's no need to forbid or even avoid that.

Collapse
 
kristoff profile image
Loris Cro • Edited

I personally consider (a) and (b) to be a choice that favors ease of writing over reading.

As for the generic stuff, I should have specified that the distinction helps when jumping in the middle of a big file with lots of code in it. Imagine jumping somewhere in github.com/ziglang/zig/blob/master... and trying to make sense of things: being able to tell if the method that you're looking at is of a generic struct or not can be of help.

Of course it's not fundamental help, but that seems to me a stronger concern than using Self because you often change the name of your types.

nope, it's needed when you have an unnamed ("anonymous") struct

In practice, if you have a non-generic anonymous struct, you can always bind it to a name, which means that you don't have to use @This(). With generic structs you don't have that option. There might be other concerns that might make you prefer to avoid creating a separate decl for it, but in terms of necessity, it's only generic functions, as far as I know.

Collapse
 
jibal profile image
jibal • Edited

in the middle of a big file with lots of code in it. Imagine jumping somewhere in github.com/ziglang/zig/blob/master... and trying to make sense of things

You specifically wrote about small structs ... those are easy to read in any case.

if you have a non-generic anonymous struct

It's common to return a struct without binding it to a name. The generic parameter might be used to choose which struct to return, without the struct itself being generic.

Like I said, anonymity is the issue, not genericity.

I believe my points are valid and have not been refuted. My main point is that "Don't do this!" (as opposed to simply explaining why it's not necessary) is uncalled for. Over and out.

Thread Thread
 
kristoff profile image
Loris Cro • Edited

No, it's only generic structs.

This struct has a nested anonymous struct. If you keep it there, you will have to use @This() in its methods.

const Foo = struct { 
    a: struct {
      b: usize,
   },
};
Enter fullscreen mode Exit fullscreen mode

But you can always move it to a separate declaration, in which case @This() won't be needed anymore.

const Foo = struct { 
    a: Bar,

    const Bar = struct {
      b: usize,
   };
};
Enter fullscreen mode Exit fullscreen mode

In generic structs you can't do the same because binding the struct definition to a variable in a non-declarative scope doesn't make the variable name available right away.

This in fact won't compile, and the only way to make it compile is to use @This():

pub fn Foo() type {
    const Bar = struct {
        x: usize,
        fn baz(self: Bar) void {}
    };

    return Bar;
}
Enter fullscreen mode Exit fullscreen mode
gen.zig:4:22: error: use of undeclared identifier 'Bar'
        fn baz(self: Bar) void {}
Enter fullscreen mode Exit fullscreen mode

So, yes, it's only generic functions.

Be careful that your eargerness to win the argument is making you lose the only fight that matters :^)

Thread Thread
 
jibal profile image
jibal • Edited

Be careful that your eargerness to win the argument is making you lose the only fight that matters :^)

You're being rude, poorly representing Zig, and are projecting ... it is you who are trying way too hard, and falling into intellectual dishonesty.

The only reason that you have to use @This() is because the binding of structs to names is a hack that only occurs at the top level. The language could allow
Bar to be used inside of Foo.Bar. And again, it has nothing to do with genericity ... Foo and Bar aren't generic in your example. You could move Bar to the top level, use Bar within it, and have Foo return it and it would be semantically identical.

I am done here and won't respond further.

P.S.

Since Bar wasn't generic in the original example, you can move the struct definition out of the function body and just use the decl name directly:

No, it's because there's a syntax limitation in Zig such that you can only use the name of a struct inside of that struct at the top level, not within inner structs. That it has nothing to do with genericity is proven by the fact that the syntax error was generated in the previous non-generic version. Adding a generic parameter changes nothing, it's only a matter of someone trying too hard to win a lost argument.
// this will now work -- because Bar is now at the top level, where it is allowed, like I said.

Thread Thread
 
kristoff profile image
Loris Cro • Edited

Just for some extra clarity for the other people reading:

pub fn MakeBar(comptime T: type) type {
    const Bar = struct {
        x: T,
        fn baz(self: Bar) void {}
    };

    return Bar;
}
Enter fullscreen mode Exit fullscreen mode

This is now generic and won't work without @This(). The previous example showcased this same thing, I just didn't bother adding a generic parameter as the example seemed sufficiently self-evident as is.

But, if you think about it, the previous example does serve as yet another example of why it's genericity that forces you to use @This(). Since Bar wasn't generic in the original example, you can move the struct definition out of the function body and just use the decl name directly:

pub fn Foo() type {
   return Bar;
}
const Bar = struct {
   x: usize,
   fn baz(self: Bar) void {} // this will now work
};
Enter fullscreen mode Exit fullscreen mode

Now, onto moderation: @jibal please go solve your anger issues in another community, consider your account permanently suspended.

Thread Thread
 
flipbit profile image
Max • Edited

Loris, thank you for your article
EDIT:
I continued:

but please, get a grip and apologize and unsuspend jibal. this is symbolic.

Sorry, I didn't take into account what might have been said before the posts of jibal were edited.

Collapse
 
drew profile image
Drew • Edited

@This() is needed when you are implementing a generic structure.

It's not needed in the example you provided. You could also use Box(T) instead of @This().

pub fn Box(comptime T: type) type {
    ...
        pub fn unlock(self: @This()) T {
Enter fullscreen mode Exit fullscreen mode

...is the same as...

pub fn Box(comptime T: type) type {
    ...
        pub fn unlock(self: Box(T)) T {
Enter fullscreen mode Exit fullscreen mode

I do agree that there is a risk of forming an incorrect mental model when using Self. It's a risk that comes with any abstraction, so we need to use abstractions carefully.

But I also find that using Self instead of @This() makes the code more readable, especially when it is being combined with other symbols or passed into functions.

Note: I prefer @This() over the struct name because using the struct name forces me to keep more names in my working memory, and it can often be very verbose (such as with generics). So the rest of this comment is just an argument about Self versus @This() but you can also consider @This() to be a proxy for the struct name.

The problem with Self is that there is less clarity about what Self means when compared to @This(). The meaning of Self depends on how it is defined in the particular context, whereas @This() has only one definition. The cost here is the risk of making an incorrect assumption about its meaning, or the time spent checking the definition.

That said, it is very rare that you'll find it defined in any way beside const Self = @This();, so personally I find the aforementioned costs to be negligible. I choose to use this pattern because I feel that it reduces boilerplate and improves readability where it is used. Yes it adds an extra line of code, but it reduces characters from many other lines of code.

The biggest place where this falls apart is with nested structs. If you define Self in an outer struct, inner structs will be unable to shadow the name, and the name has a confusing meaning in those scopes. So I am careful not to use this pattern in a struct that contains additional structs within it. As long as developers are principled in how they use this pattern, there is no problem.

In my eyes, all these arguments actually point towards a missing feature in the language. The name Self almost always has the same meaning anywhere it is used, and it would obviously be poor style to use it to mean something else. All the downsides of the pattern would be fixed if it were elevated to a keyword that is equivalent to @This(). Developers would no longer be required to be principled to ensure that we can all easily form a proper mental model. The meaning would be clear and consistent, as guaranteed by the language itself.

Developers have already expressed a preference in their code by developing a common practice that has turned Self into a pseudo-keyword. This includes the authors of the standard library, where this pattern is used. So I propose we just make Self a proper keyword and get rid of @This().

Collapse
 
htqx profile image
thanks you see

I don't see a proble

Collapse
 
mkulak profile image
Misha

so let's talk fist about when @This() is necessary.

Typo: "fist" -> "first"

(IDK how to report it other than leaving comment here)

Collapse
 
msw profile image
Martin

But is there a harm to this "malpractice" ?

Collapse
 
kristoff profile image
Loris Cro

For the person writing the code: you form a wrong mental model of how Zig works. Not a huge problem, but if it's not too costly, it's always better to have a more precise mental model, and in this case it takes very little to fix one's mental model.

For the people reading the code: see the very last paragraph of the post.

Collapse
 
himujjal profile image
Himujjal Upadhyaya

Not just for simple structs. I feel writing struct names as it is for every struct makes it very clear. LSP also helps in this case

Collapse
 
kristoff profile image
Loris Cro

That's fair, I used simple in the title because it sounds better than "non-generic" but it really is advice for all structs where you can get away with not using @This().

Some comments may only be visible to logged-in visitors. Sign in to view all comments.