Loving zig so far, but I do have one minor concern

I’m very new to zig and have been going through ziglings. So far I think that Zig is the best thing since C. However, there is just one thing I am concerned about: Globals.

I was doing 029_errdefer when I noticed towards the end of the file:

// Sneaky, weird global stuff.
counter += 1;

And I thought to myself: Being able to define globals is great. By all means give me the gun that I will shoot my self in the foot with. But why make it so easy, or so opaque? In this instance the function is short so you can see there is no counter defined in the local context. But if it were at the end of a much longer function and the comment was not there it could cause needless headaches. I think accessing the global scope is rare, and in a language where standard practice is to be as verbose as:

std.debug.print("Hello world!")

It seems out of place to make accessing a global so simple. I would suggest requiring a keyword like this:

// Loud and proud weird global stuff.
global.counter += 1;

This way its obvious I’m using a global and I can even search for this keyword to find any usage of a specific global or globals in general.

I went on Github to file a feature request but it seems Zig is not accepting new language proposals. So I came here to lobby for a feature like this.

I hope its not too forward.


For reference here is a link to the complete ziglings excersize: ziglings/exercises/029_errdefer.zig at main · ratfactor/ziglings · GitHub

And below is the contents (incase it is deleted or changes)

//
// Another common problem is a block of code that could exit in multiple
// places due to an error - but that needs to do something before it
// exits (typically to clean up after itself).
//
// An "errdefer" is a defer that only runs if the block exits with an error:
//
//     {
//         errdefer cleanup();
//         try canFail();
//     }
//
// The cleanup() function is called ONLY if the "try" statement returns an
// error produced by canFail().
//
const std = @import("std");

var counter: u32 = 0;

const MyErr = error{ GetFail, IncFail };

pub fn main() void {
    // We simply quit the entire program if we fail to get a number:
    var a: u32 = makeNumber() catch return;
    var b: u32 = makeNumber() catch return;

    std.debug.print("Numbers: {}, {}\n", .{ a, b });
}

fn makeNumber() MyErr!u32 {
    std.debug.print("Getting number...", .{});

    // Please make the "failed" message print ONLY if the makeNumber()
    // function exits with an error:
    std.debug.print("failed!\n", .{});

    var num = try getNumber(); // <-- This could fail!

    num = try increaseNumber(num); // <-- This could ALSO fail!

    std.debug.print("got {}. ", .{num});

    return num;
}

fn getNumber() MyErr!u32 {
    // I _could_ fail...but I don't!
    return 4;
}

fn increaseNumber(n: u32) MyErr!u32 {
    // I fail after the first time you run me!
    if (counter > 0) return MyErr.IncFail;

    // Sneaky, weird global stuff.
    counter += 1;

    return n + 1;
}
```zig
2 Likes

One option, if you are concerned with mutable globals being sneaky, is to name them like COUNTER.

2 Likes

I agree with this post as a practical place to start.

The issue I think that the OP is eluding to actually shows up (in practice) in larger projects (especially ones that utilize a multi-programming model of some sort… threading… etc…).

In a very large project (I’ve worked on million+ line projects before) I can’t think of a “non-sneaky” global that I’ve ever come across. In reality, if you don’t know that some global is being modified half-way across the project in some function you’ve never read before, you can name it whatever you want but you’re still going to have the same issue.

Because of this, for large projects that have high cognitive burden in their general architecture, I don’t see that naming something different truly solves the problem (although, I agree that it is a step in the right direction, no argument there). It could be named I_AM_A_GLOBAL_DONT_MODIFY_ME and if someone is modifying it in a function I’ve never read (it may be simply not accessible given time constraints and project sizes), then it doesn’t really matter what it’s called.

At some level, functional purity where outside state is prohibited seems like the only “certain way” to handle this aspect… but even then, if you trace all the places that a variable can flow through a system in the form of an argument, it gets complex in an entirely different way altogether.

1 Like

Maybe I’m biased by history but to me all caps identifiers scream, “I AM A CONSTANT”. So it seems somewhat counter intuitive to me to differentiate global state with all caps. If its up to me, I prefer my identifiers not to scream at me at all, though its not always possible.

More importantly all caps are harder to search for. You have to write a regex, and you will probably get more false positives. And being just a convention it comes with little guarantee. It’s not strong enough in my opinion.

But please also consider that not all identifiers in the global scope should be prefixed. There is no need at all to prefix a global const when using it:

const glib = @import("glib")

const config_file = "config.lma";
var problem_set: []u8 = undefined;

pub fn main() !void {
    const config = glib.loadConfigFrom(config_file);
    glib.loadProblemData(global.problem_set, config.data_url);
    glib.doImportantStuff();
}
1 Like

In almost all my projects where I use mutable globals, I wrap them in a “global” struct like this:

const global = struct {
    pub var counter: usize = 0;
};

pub fn main() !void {
    global.counter += 1;
}

It’s not enforced by the language but I can still make sure I do it myself. My only concern with adding a requirement to do this in the language would be whether this would prevent otherwise good/valid use cases. You’d probably only want to enforce it for runtime/mutable data for example, otherwise all declarations like struct/type aliases would also require a “global” keyword. I think the idea is worth exploring more.

10 Likes

const global = struct {
pub var counter: usize = 0;
};

I’m glad marler8997 pointed this out - I prefer this as well.

also another solution might be the following:

const globals = @import(“globals.zig”);

The const tag won’t prevent you from changing the pub variables it declares, but it also does the namespacing.

4 Likes