We cannot have local var names inside a struct function that are equal to a field in that struct.
Mostly that is strange because we always have to access that field with self.xxx.
So is that restriction only because of the phenomenon “files are structs”?
(In that case we have no thisfile.xxx)
The absence of identifier shadowing in Zig seems to be more a decision to improve readability than anything else. Zig guarantees that an identifier, once declared in a scope, always refers to the same object in that scope and all of its descendant scopes. Moreover, it’s been decided that declarations and fields share the same namespace, to allow for decl literals (the thing where you can call .init() and it infers the namespace from the result type).
I don’t think there is anything technically preventing Zig from supporting shadowing of identifiers. It was a purposeful decision to not allow it.
Actually we can. What is not possible is having local variables with the same name as global variables namespaced in the struct, because these are accessed without self.xxx.
const S = struct {
field: i32,
var global: i32 = 3;
pub fn func(self: *S) void {
self.field = 5; // accesses the struct field
var field: i32 = 42; // no problem
global = 13; // accesses the global variable
var global = 12; // ERROR
}
};
See, this is where the argument breaks down for me. Every single time I try to shadow, it’s intentional. In fact, it usually prevents future errors, because it narrows down the scope of things. And I’m too old and stupid to deal with wide scopes of things.
In languages that allow shadowing I try to to use it to immediately eliminate a no-longer-necessary variable from the namespace:
const path = "file path";
const aw_wtf = std.path.windowsParsePath(path);
// oh gosh, I sure like having `path` still available in my namespace
std.fs.whatever(pa^H^Haw_wtf);
I still feel it’s clearer. thingie_opt truely is different than thingie, and that difference shows in the name. Plus, often this thingie_opt is the return from some function, in which case you can just do
if (foo(a,b)) |thingie| {
bar(thingie);
}
Things are even more interesting with compound-optional-error returns:
while (foo(a,b)) |result| if (result) |thingie| {
bar(thingie);
} else break else |err| switch (err) { ...
For example, with reader’s takeDelimiter('\n') and others like it,
while(reader.interface.takeDelimiter('\n')) |result| if (result) |line| {
std.debug.print("line: {s}\n", .{line});
} else break else |err| switch (err) {
error.ReadFailed => {
return reader.err.?; // return the specific error; especially essential for error.Canceled
},
else => return err,
}
This is a special case in which the const (or var) name and block name can’t conflict, so you can just use the same name throughout, if that’s not a mental bother.
This feels a bit wrong. Don’t you want to separate the levels of abstraction? How do tests fit into it? You do not need to duplicate var names in the same function if you have separate functions:
fn abstractWhatever(reader: *std.Io.Reader) !void {
while(try reader.takeDelimiter('\n')) |line| {
std.debug.print("line: {s}\n", .{line});
}
}
test abstractWhatever {
var reader: std.Io.Reader = .fixed("test\ninput\n");
try abstractWhatever(&reader);
}
fn fileWhatever(file: std.Io.File, io: std.Io) !void {
var buffer: [1024]u8 = undefined;
var reader = file.readerStreaming(io, &buffer);
abstractWhatever(&reader.interface) catch |err| switch (err) {
error.ReadFailed => {
// return the specific error
// especially essential for error.Canceled
return reader.err.?;
},
else => return err,
}
}
Yes, sure, that’s fine, and has advantages/tradeoffs. My point was to illustrate how ‘result’ is different than ‘line’ (unwrapped) (which is also different from ‘err’), so different names can be useful. (i.e., those three names represent the different compound optional ingredients.) Whether it’s a good idea to do everything in one block of code like that is up to the coder and use case, I guess.