Hi! I wonder why Zig doesn’t prevent circular imports. Wouldn’t such a restriction speed up builds and make the build system simpler just like it does in Go?
Wouldn’t that break everywhere?
E.g. just take a random file from the standard library, and you will notice that the first line is something like const std = @import("../std.zig");
, which is creating a circle.
Well yes, if they were to change it now, it might break a lot of stuff, but that doesn’t explain why they allowed it in the first place.
I’m going to take a wild (uneducated) stab in the dark and guess it has to do with C compatibility.
Avoiding cyclic dependencies would probably be difficult because there is usually no separation between interface and implementation in Zig.
Not an answer to why per-se, but I like that there are circular imports, It means i can group things how i think is natural, without having to add extra files just to satisfy the compiler. Note that circular dependencies are not allowed, for obvious reasons.
I compare it with Python. In python an import of a module is an inherent dependency on everything in that module. In zig an import is just a handle that allows you to namespace or use specific items from that file, without depending on everything else in that file.
My question is, why would you want to restrict circular imports?
- Build speed? Zig builds fairly fast, and this wont make much of a difference either way. The main slow down is LLVM, which is being addressed right now. (custom backend with incremental compilation)
- For code clarity? I don’t think its a problem, its fairly intuitive especially with the struct base of imports.
- Also a real thing is developer experience, and not needing to worry about import order is really nice.
- Zig is also lazily analized, so it is not making any huge restrictions on the build system itself (to my knowledge)
no, zig evaluates everything lazily, only the first used import of a file that zig sees triggers evaluation, though only for what is being used, latter uses will be a lookup unless it’s something that hasn’t been used before.
internally sure. It wouldn’t make using it simpler, and it would make code structure more complicated
In the far past I was using Delphi (later moved to C# and Zig).
In Delphi you somwtimes have to code things in a strange way to prevent circular references.
Not having ‘circular’ references is a blessing in my opinion. Not everything is a tree.
I would rather ask, why not allow cyclic imports? I would assume that Zig keeps track of already imported modules, and doesn’t import another time when it encounters the same import, which automatically breaks any cycles - also the compiler only picks the stuff from imported modules that’s actually used, so an import is probably more like populating a ‘table of contents’ instead of doing actual compilation work.
Keeping track of imports to check for cyclic imports (just to throw an error) sounds like it’s at least the same effort for no additional gain (since the redundant imports are dropped anyway - assuming that the Zig import system works like I think it does).
When modules are separately compiled there is a chicken-egg problem because the first module in compilation order depends on others that are not build yet.
Zig compiles everything as a root module and there is no cyclic import problem.
Rob Pike (creator of Go, a language known for very fast compilation) cites the banning of circular imports and unused imports as reasons for why the compiler can be made so fast.
I tend to find it harder to understand a codebase with circular imports as it forces you to read in spirals instead of up and down a tree with branches.
If two modules really depend on each other and can’t be separated with dependency injection, then they should in many cases be in the same module.
I like circular. A program’s structure is never a 100% clean tree. At least my programs are not
Restructuring satisfying the non-circular requirement can also make code less readable, because things that belong together logically must be splitted.
EDIT: oh I already wrote something like this earlier…
Zig doesn’t have packages in the same sense that Go does. It has containers. Every struct is a container, but also every file is a container. Imports only exist in Zig as variable definitions where the value is a container (using @import()
to reference file containers).
Saying you should not be able to have circular imports in Zig really means your should not be able to have circular container references. The claim you are making in Zig would have to apply to all containers: structs that depend on each other and can’t be separated with dependency injection should be the same struct. This is very wrong; even Go allows this kind of recursive reference between structs, so long as it is within the same package.
Personal experience here: during most of my time with Go, the cyclic import restriction was more a hindrance than a help, whereas having the ability to be “cyclic” in Zig has actually been useful. If two files (that may analogously represent different packages in Go) need something from each other, especially something small, I don’t need to make another file to just dump that thing into to avoid a cycle. That’s nice.
People seem to like cycles in their code here. I don’t, but let’s just agree to disagree :).
Sure, ”cycles” in the same package is fine in Go, but that is not considered a cycle.
If you do not consider that a cycle, than there are no cycles in Zig either.