Currently, zig modules are struct files by default. This allows the following to compile:
// foo.zig
const std = @import("std");
a: u32,
b: usize,
// bar.zig
const f = @import("foo.zig"){ .a = 4, .b = 6 };
This was the initial design of the language because adding an extra type of declarative scope that didn't allow fields would be a new concept and would make the language more complicated. But with opaque namespaces, we now have declarative scopes that do not allow fields.
This is likely to be contentious, but my opinion is that the option to write a module either as a type to be instantiated or a namespace containing a type makes Zig code more difficult to read. For every library the reader must determine what style is used for each module. And the only way to determine that when reading the module is to scan the document for field declarations, which do not contain a searchable keyword and can occur anywhere in the file.
Though it would be breaking, I think it would improve the readability of Zig code to switch module scope from struct to opaque.
Using opaque would still allow one to instantiate an import, and would allow one to write C-wrappers using the same trick.
You can create pointers to opaque types but you cannot create instances of them. Yes, the imported type is still a type, and @This() is still valid at top level scope, but this change gives the hard guarantee that there are no field declarations at top level scope, so you don't need to search for them when reading code.
I've started taking advantage of files-as-structs in the self hosted compiler: https://github.com/ziglang/zig/tree/master/src
(The files with that start with capital letters)
I have to say I've found the pattern quite nice and I'm in favor of status quo so that I can keep doing it. I also wanted to move std.fs.File and std.fs.Dir to this pattern.
@ifreund also uses this pattern and appears to be happy with it: https://github.com/ifreund/river/tree/master/river
I usually want to limit myself to one major struct per file in order to keep things organized. Since that's the case, using toplevel fields makes sense to me as it keeps the indentation level low and makes @import() statements less redundant. Following the TitleCase vs snake_case naming convention for files serves as a strong hint as to whether the reader should look for toplevel fields or not.
Since I like toplevel fields I'm against this proposal, but if it were to be accepted I would further propose requiring structs to have fields and using opaque for all namespacing. Although, the keyword might be worth reconsidering in that case.
Ok, you guys have read and written a lot more Zig than me so I'll defer to your experience here. I'll leave this issue open for a couple days in case anybody new weighs in with good points we haven't considered, but in lieu of that I think it can be closed.
I don't find top-level structs readable, but if we were to remove them I think it'd be better to just disallow top-level fields in files.
I think this is the key:
Following the TitleCase vs snake_case naming convention for files serves as a strong hint as to whether the reader should look for toplevel fields or not.
There's no reason it needs to be difficult to differentiate.
While I kind of agree with the proposal in terms of style (I will probably not take advantage of top level fields personally) maybe there's a way to just limit the proposal to the entry-point of a package?
Within a the implementation of a given project / library (e.g. the stage2 source Andrew linked above) I think sticking to the convention of TitleCase vs snake_case is enough to tell the difference and it definitely seems useful. Same for if a module is distributed as a single file. It's also normal to look at the implementation rather than just docs/function signatures when working within a project so opening up a file to check isn't that much more work.
The only real place where it would be confusing is importing an external package (e.g. std, sdl2, etc). In this case I think it would make more sense to have the entry point (e.g. pkg/src/lib.zig) be a strict opaque where types exported by the package are defined (see https://github.com/ziglang/zig/blob/master/lib/std/std.zig as an example). If the types happen to be defined using file-level fields or as a struct in a file who cares; it's an implementation detail. Users would just see it as namespace.Type as always.
While I understand the appeal of the file-as-struct pattern, I don't feel it is great to encourage this in the long term because it goes against the "one obvious way to do things" philosophy: using a pub const Struct = struct { ... }; works just as well, and is more consistent when you have literally any modifiers, need an enum/union, or are providing a generic type. I feel and have personally experienced that it is quite possible and practical for a revision to an initial draft of a codebase to add a modifier to a type, change its TypeId, or make it a generic type; none of these are compatible with the file-as-struct pattern, and if the file-as-struct pattern was in use, it will require a refactor of greater scope than would be necessary if you just used a decl like everything else.
I'm not saying I am against the status quo of namespaces being struct, as structs work fine as namespace types (they're zero-sized when they have no fields, as well, unlike opaque). However, I wanted to express my opinion that the file-as-struct pattern is not a good stylistic decision and goes against the Zig zen, and therefore I feel this issue is worth reopening and discussing further.
Since this is reopened I should note that my opinion has shifted a bit. I still think that top-level members are kind of weird, but I think using opaque to try to replicate namespaces is even weirder. So I think if we want to disallow top-level fields we should
instead introduce proper namespace types into the language, and have the default scope be that. We would also introduce the ability to do const foo = namespace struct { ... }; or something similar in a file, to avoid file scope being a special case. In lieu of that, I think the status quo is better than using opaque. struct is the canonical declarative scope in Zig, and having the default scope be anything else that isn't properly a namespace just feels like a clever hack to me.
See also #1047, where it was decided to do the opposite, and #2022, which explicitly allowed top-level fields.
Most helpful comment
While I understand the appeal of the file-as-struct pattern, I don't feel it is great to encourage this in the long term because it goes against the "one obvious way to do things" philosophy: using a
pub const Struct = struct { ... };works just as well, and is more consistent when you have literally any modifiers, need anenum/union, or are providing a generic type. I feel and have personally experienced that it is quite possible and practical for a revision to an initial draft of a codebase to add a modifier to a type, change itsTypeId, or make it a generic type; none of these are compatible with the file-as-struct pattern, and if the file-as-struct pattern was in use, it will require a refactor of greater scope than would be necessary if you just used a decl like everything else.I'm not saying I am against the status quo of namespaces being
struct, as structs work fine as namespace types (they're zero-sized when they have no fields, as well, unlikeopaque). However, I wanted to express my opinion that the file-as-struct pattern is not a good stylistic decision and goes against the Zig zen, and therefore I feel this issue is worth reopening and discussing further.