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});
}
}
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});
}
}
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 {
// ...
}
};
}
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 {
// ...
}
};
}
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
};
}
};
}
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});
}
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 (23)
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()
.True!
I use files as structs a lot, so I was confused by this article until your comment. Thanks!
I'd be interested in your thoughts on this counterargument.
Focusing locally on a struct method, is there a bug here?
Well, maybe, this shows how there could've been a copy-paste error, which I believe would compile:
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!
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: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.
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
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.
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 :^)
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.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 othercomptime
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.)There's no need to forbid or even avoid that.
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.
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.You specifically wrote about small structs ... those are easy to read in any case.
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.
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.But you can always move it to a separate declaration, in which case
@This()
won't be needed anymore.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()
: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 :^)
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 allowBar
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.
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.Just for some extra clarity for the other people reading:
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()
. SinceBar
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:Now, onto moderation: @jibal please go solve your anger issues in another community, consider your account permanently suspended.
Loris, thank you for your article
EDIT:
I continued:
Sorry, I didn't take into account what might have been said before the posts of jibal were edited.
I'm skeptical of this advice. Using
Self = @This()
(or just@This()
everywhere) makes your code robust to the struct name changing--or other edits down the line.So, there's a benefit in using
Self
as an alias. What's the cost that makes it a bad trade off for "simple" structs?It's not needed in the example you provided. You could also use
Box(T)
instead of@This()
....is the same as...
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 aboutSelf
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 whatSelf
means when compared to@This()
. The meaning ofSelf
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 makeSelf
a proper keyword and get rid of@This()
.I don't see a proble
Typo: "fist" -> "first"
(IDK how to report it other than leaving comment here)
But is there a harm to this "malpractice" ?
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.
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
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.