Global functions and methods clash into ambiguity. But why?

Here’s a simple example:

fn foo(a: u8, b: u8) u8 {
    return a + b;
}

const Bar = struct {
    fn foo(c: u8) u8 {
        const x: u8 = 1;
        const y: u8 = 2;
        return foo(x, y) + c;
    }
};

Trying to compile this will give the following error:

.../src/main.zig:9:16: error: ambiguous reference
        return foo(x, y) + c;
               ^~~
.../src/main.zig:6:5: note: declared here
    fn foo(c: u8) u8 {
    ^~
.../src/main.zig:1:1: note: also declared here
fn foo(a: u8, b: u8) u8 {
^~

Why is this call ambiguous? Both functions indeed do have the same names, but the signatures are clearly different. And if the signatures had been the same, how would I have called the global function?

Zig just doesn’t like name shadowing, regardless of the signature or other details.

Okay, but the functions are from different namespaces. Why is it shadowing? And how would I call the global function here?

“Shadowing” is exactly this - using same name in global namespace (in this case) and in nested namespace, as I understand.

But… it’s a struct, not a scope. I would imagine the Bar’s method to be recognized as Bar.foo instead of foo here. Hence, calling foo would call foo, and never Bar.foo.

Agreed. Compiling this

pub fn main() void {
    var k: u32 = 2;
    k = k * k;
    var j: u32 = 0;
    while (j < 3) {
        var k = j;
        j += 1;
    }
}

results in

$ /opt/zig-0.10/zig build-exe shadow.zig 
shadow.zig:7:13: error: local variable 'k' shadows local variable from outer scope
        var k = j;
            ^
shadow.zig:3:9: note: previous declaration here
    var k: u32 = 2;
        ^

i.e the word “shadow” is explicit.

But “structurally” it is the same - same identifier in outer/inner scope, same identifier in outer/inner namespace… something like that I guess.

Now, here’s another example. Let’s just put everything in another namespace:

const std = @import("std");
const expectEqual = std.testing.expectEqual;

const Bar = struct {
    fn foo(x: u8) u8 {
        return x;
    }
    const Foo = struct {
        fn foo(x: u8) u8 {
            return x;
        }
    };
};

test "Fine?" {
    const a = Bar.Foo.foo(1);
    const b = Bar.foo(2);
    try expectEqual(a, 1);
    try expectEqual(b, 2);
}

Suddenly, it’s all right. There is no ambiguity. But how is it conceptually different from the first example?

1 Like

Forgot to call foo there:

const std = @import("std");
const expectEqual = std.testing.expectEqual;

const Bar = struct {
    fn foo(x: u8) u8 {
        return x;
    }
    const Foo = struct {
        fn foo(x: u8) u8 {
            return Bar.foo(x);
        }
    };
};

test "Fine?" {
    const a = Bar.Foo.foo(1);
    const b = Bar.foo(2);
    try expectEqual(a, 1);
    try expectEqual(b, 2);
}

But yeah, would be nice to have a way to reference a global function in a similar way I have accessed Bar.foo here.

Not quite a similar way, but works:

const std = @import("std");

fn foo(a: u8, b: u8) u8 {
    return a + b;
}
const globalFooFnPtr = *const fn(u8,u8) u8;

const Bar = struct {

    gFoo: globalFooFnPtr,

    fn init(func: globalFooFnPtr) Bar {
        return Bar {
            .gFoo = func,
        };
    }

    fn foo(self: *Bar, c: u8) u8 {
        const x: u8 = 1;
        const y: u8 = 2;
        return self.gFoo(x, y) + c;
    }
};

pub fn main() void {
    var b = Bar.init(&foo);
    std.debug.print("b.foo(5) = {}\n", .{b.foo(5)});
}

Well, yeah, but that’s just polluting the struct with extra (non-free) fields…

4/8 bytes… ok, if it is a problem, why not to use different name for global foo, like globalFoo()?..

Yep, that’s the workaround I have recently used. Although, I changed the name of the method. That’s just a bit annoying, since there is seemingly (IMHO) no reason for it to be this way.

I think Zig would like you to code like this:

fn globalFoo(a: u8, b: u8) u8 {
    return a + b;
}

const Bar = struct {
    fn foo(c: u8) u8 {
        const x: u8 = 1;
        const y: u8 = 2;
        return globalFoo(x, y) + c;
    }
};

What would be the disadvantage of being explicit like this? In an Object Oriented language you could probably get away with using the same name foo in both cases, but does that make the code better? I suppose it makes it easier to write, but not easier to read and unambiguously understand what’s going on; especially if a year has gone by, or the call site is 10,000 lines apart from the definition of the global.

That’s okay, I’m fine with doing that. Although I do think it would be easier to read.

All of this confusion comes from the fact that Zig is okay with methods calling methods without specifying the namespace:

const std = @import("std");
const expectEqual = std.testing.expectEqual;

// fn foo(a: u8, b: u8) u8 {
//     return a + b;
// }

const Bar = struct {
    fn foo(a: u8, b: u8) u8 {
        return b - a;
    }
    fn bar(c: u8) u8 {
        const x: u8 = 1;
        const y: u8 = 2;
        return foo(x, y) + c;
    }
};

test "Fine?" {
    const c = Bar.bar(1);
    try expectEqual(c, 2);
}

Now that you mentioned it, I was indeed leaning towards things I got used to in OOP. But I can also understand the Zig’s point of view. I guess that’s the matter of taste at this point.

1 Like

… but why use global functions in “main” (or not very “main”) file at all?
Suppose that global function is some utility/helper function. Okay, let put them
all into separate file (== namespace):

// util.zig 
const std = @import("std");
pub fn doThis() void {
    std.log.info("I am doing this", .{});
}
pub fn doThat() void {
    std.log.info("And I am doing that", .{});
}
// main.zig
const util = @import("util.zig");
pub fn main() void {
    util.doThis();
    util.doThat();
}

That’s totally fair, but in my case I need them in one file, so it’s easier to share.

That’s interesting, I would have expected it to be consistent with the global stuff. This might be an oversight either in the design or the implementation. I’ll ask Andrew when I get a chance.

Just saw this issue. Maybe not exactly the same, but a similar problem?

In a sense. That issue, #705 and #1836 are about declarations and fields having the same name at call site (going as far as proposing a special syntax for this), and this one is about (potential) ambiguity of calling declarations with the same name in a scope (e.g., in Foo’s scope above you have two foo’s).