Duplicate var names

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.

1 Like

You can still totally use @This().xxx

3 Likes

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
    }
};
4 Likes

Ow damn… you are right.
Edit: Aha…
We cannot use the name of a function. THAT was the thing i encountered.
And that is logical.

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);

See also:

if (thingie) |cant_call_this_thingie| {
    // but sure wish I could
}
4 Likes

yes. very annoying but logical. I always am short of useful names in that case.

1 Like

FWIW, you can use block expressions for that (in most cases):

const aw_wtf = blk: {
	const path = "file path";
	break :blk std.path.windowsParsePath(path);
};

// `path` doesn't exist, it can't hurt me :-)

std.fs.whatever(aw_wtf);

… except for this, yes:

But at that point, I usually rename thingie to thingie_opt, then capture it as thingie:

if (thingie_opt) |thingie| {
    // happy, happy, happy :-)
}
1 Like

Yep, same. But at that point we’re in Hungarian notation territory, and no offense to Magyar people, but surely we can do better!

2 Likes

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,
      }

You can also do:

const aw = aw: {
	const path = "file path";
	break :aw std.path.windowsParsePath(path);
};

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.

1 Like

It also may make sense to do this, depending on context:

const path = path: {
	const path = "file path";
	break :path std.path.windowsParsePath(path);
};

Since a function-level identifier (as opposed to a type-level one) doesn’t exist until after its declaration, this is still completely legal.

2 Likes

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.

1 Like