-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove usingnamespace
#20663
Comments
How do you suggest to remove the usingnamespace in c.zig? Seems to me the proposed suggestion leads to a lot of code duplication. |
Good point, I should have addressed that. The proposed suggestion leads to relatively little duplicated code in reality. The bulky things are the definitions: those are just moved from
We have already started some parts of this migration. It'll certainly be tedious to complete, but I don't think it will make the Migrating this code is naturally a prerequisite for this proposal: so, if that migration goes poorly, it will of course give us a good reason to rethink this proposal. |
I propose instead of moving everything into a single
|
I have read that proposal before in Discord (I think every time such comments were started by mlugg?) and complete proposal here. For now I have very negative opinion about removing |
See "Implementation Switching" in the OP. See "Conditional Inclusion" in the OP. |
@BratishkaErik, here's an attempted translation of the
By the way, please stop marking everything as |
The «mixins» example went more complicated to read. That breaks the idea about readability that you has mentioned. |
I rewrite just 2 functions from zalgebra as how I understood Implementation Switching. pub usingnamespace switch (dimensions) {
2 => struct {
pub inline fn new(vx: T, vy: T) Self {
return .{ .data = [2]T{ vx, vy } };
}
pub inline fn toVec3(self: Self, vz: T) GenericVector(3, T) {
return GenericVector(3, T).fromVec2(self, vz);
}
},
3 => struct {
pub inline fn new(vx: T, vy: T, vz: T) Self {
return .{ .data = [3]T{ vx, vy, vz } };
}
},
4 => struct {
pub inline fn new(vx: T, vy: T, vz: T, vw: T) Self {
return .{ .data = [4]T{ vx, vy, vz, vw } };
}
pub inline fn toVec3(self: Self) GenericVector(3, T) {
return GenericVector(3, T).fromVec4(self);
}
},
else => unreachable,
}; After: pub const new = switch (dimensions) {
2 => struct {
inline fn new(vx: T, vy: T) Self {
return .{ .data = [2]T{ vx, vy } };
}
}.new,
3 => struct {
inline fn new(vx: T, vy: T, vz: T) Self {
return .{ .data = [3]T{ vx, vy, vz } };
}
}.new,
4 => struct {
inline fn new(vx: T, vy: T, vz: T, vw: T) Self {
return .{ .data = [4]T{ vx, vy, vz, vw } };
}
}.new,
else => unreachable,
};
pub const toVec3 = switch (dimensions) {
2 => struct {
inline fn toVec3(self: Self, vz: T) GenericVector(3, T) {
return GenericVector(3, T).fromVec2(self, vz);
}
}.toVec3,
3 => @compileError("toVec3 not defined for GenericVector(3, " ++ @typeName(T) ++ ")"),
4 => struct {
fn toVec3(self: Self) GenericVector(3, T) {
return GenericVector(3, T).fromVec4(self);
}
}.toVec3,
else => unreachable,
}; I personally see this as worse variant for both readability and compile errors. src/generic_vector.zig:83:27: error: struct 'generic_vector.GenericVector(3,f32)' has no member named 'toVec3'
_ = &Self.toVec3;
^~~~~~
src/generic_vector.zig:33:19: note: struct declared here
return extern struct {
~~~~~~~^~~~~~ you had less lines of code to read, and ZLS works perfectly here (go-to definition sends to the actual function, autocomplete works with correct doc comments etc.). src/generic_vector.zig:71:18: error: toVec3 not defined for GenericVector(3, f32)
3 => @compileError("toVec3 not defined for GenericVector(3, " ++ @typeName(T) ++ ")"),
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ , you need to scan every function to check what is the rule for inclusion of the function (maybe it switches not only on dimensions, etc.), you have ~13 verbose struct type declaration for every function. If we'd had anonymous functions like in #4170 , it would regain some readibility (code would be ~same length as old code, and no structs-just-for-function), but will not fix compile errors. |
@BratishkaErik, please see my translation linked above. |
const x: *T = @alignCast(@fieldParentPtr("counter", m)); I don't really use mixins myself, but if I had to use the above every time - both Encouraging better namespacing is a good goal, but I think a reasonable compromise here is to bite the bullet and just support struct composition, the pattern being: const Bar = struct { _counter: u32 = 0 };
const Foo = compose(Bar, CounterMixin(Bar)); Or something. This wouldn't even need a builtin, except that function declarations can't be generated/copied with reflection right now. If there was a field like |
I saw you responce just now, sorry!
And now you have errors like "index 3 outside array of length 2" instead of more helpful "function is in fact not defined", also disadvantage in my book.
I agree here, but it would be a breaking change.
Thank you. This code is very old so I don't remember why I put it here or if it was even myself, maybe even when |
I saw this, but decided to finish my reply because did not want to lose comments by new bugged Github SPA logic or what they added to the site. Already had that experience before with review comments erased just because other comment reloaded itself. |
I need to clarify that the readability for other people could be not that you maybe assume. Generally speaking there is two type of developers. First type is usually write code in big functions with visual control of code. There is no option for them to break big function into small ones. That is because small functions require several jumps to see all code flow. And this is what they do not want to do. Second kind of developers is love small functions, but they also love functional style of coding. Zig has no any functional behaviors. And as I assume that Zig is C competitor. So we have problem here. C developers is much more the first type |
I use this pattern a lot, IIRC all thanks to nektro, but 1) not in situration where usingnamespace would work better, and 2) while |
I am divided on this issue, but ultimately, I am leaning towards removing The removal of const impl = switch(...) {...};
pub const foo = impl.foo;
pub const bar = impl.bar;
//... Conditional inclusion is an important use case for The benefits of removing |
Case of |
Thanks @mlugg for doing this writeup. Without it I wouldn't have known how to do mixins without usingnamespace. I wanted to make sure it worked and that I understood so I removed usingnamespace from my original with usingnamespace While these changes are less readable, I'm glad to know that this is still possible! 👍 It would be nice if this were a little simpler as now I must accept /tmp/tmp.zig:26:71: error: pointer computation here causes undefined behavior
const self: *const Self = @alignCast(@fieldParentPtr("m", &m));
^~
/tmp/tmp.zig:26:71: note: resulting pointer exceeds bounds of containing value which may trigger overflow |
What do you mean by "esoteric" here?
You could align the mixin field so as to omit the /// Mixin to provide methods to manipulate the `_counter` field.
pub fn CounterMixin(comptime T: type, comptime field_name: []const u8) type {
return struct {
const Ptr = *@This();
fn get(m: Ptr) *T {
return @alignCast(@fieldParentPtr(field_name, m));
}
pub fn increment(m: Ptr) void {
get(m)._counter += 1;
}
pub fn reset(m: Ptr) void {
get(m)._counter = 0;
}
};
}
pub const Foo = struct {
_counter: u32 = 0,
counter: CounterMixin(Foo, "counter") = .{},
}; It's worth noting that even in projects that use them, mixins are not commonly used: they are rarely appropriate in Zig. For instance, TigerBeetle has two "mixin-esque" types used with
This almost certainly won't ever be a thing. Composing types is awkward in terms of defining things like field merging behavior, reconciling differing struct attributes, etc. It also really lacks general usefulness. If you're trying to compose two types, we have a convenient, intuitive, existing system for doing so: fields!
Please refer to this comment for why this is not going to happen. |
Things with
If you're worried about this, you can namespace them under
But OTOH, tooling can find the definition more reliably. Even if it can't do all of the work, the first goto-def is trivial, and the second can be done by hand (or... by eyes?) very easily.
The old implementation was spread across 36 lines, while the new implementation is spread across 41 lines. That is really not a significant increase. In addition, the logic is now all actually dense; previously, similar functions -- perhaps even functions which called one another (e.g.
A "no such function" error is all well and good if you understand the implementation of the library, but is completely unhelpful if you're skimming the source code and "I can see the definition right there, why won't it work?", or if you're using some tooling like ZLS and its heuristics got the included methods wrong, etc. You can reasonably argue that the error for things like
|
@BratishkaErik with the risk of restating something obvious, I believe the code structure of the `usingnamespace` example in your comment can be completely preserved by simply explicitly listing all the re-exported declarations:
pub const DimensionImpl = switch (dimensions) {
2 => struct {
pub inline fn new(vx: T, vy: T) Self {
return .{ .data = [2]T{ vx, vy } };
}
pub inline fn toVec3(self: Self, vz: T) GenericVector(3, T) {
return GenericVector(3, T).fromVec2(self, vz);
}
},
3 => struct {
pub inline fn new(vx: T, vy: T, vz: T) Self {
return .{ .data = [3]T{ vx, vy, vz } };
}
},
4 => struct {
pub inline fn new(vx: T, vy: T, vz: T, vw: T) Self {
return .{ .data = [4]T{ vx, vy, vz, vw } };
}
pub inline fn toVec3(self: Self) GenericVector(3, T) {
return GenericVector(3, T).fromVec4(self);
}
},
else => unreachable,
};
pub const new = DimensionImpl.new; //still re-exported just as with usingnamespace
pub const toVec3 = DimensionImpl.toVec3; //still either re-exported or compile error if referenced -> analyzed |
I controversially agree that using zero-sized fields to emulate mixins is an adequate replacement for status quo, though the Also wanted to add that I feel no envy for the work the compiler has had to do to support this feature for so long. The 'subtle rules' you mention do not sound fun to write code for or design around. |
I'm here just to express that I am greatly in favor of this proposal because almost every time I encounter Mach engine is not very affected by this proposal because we have had several initiatives to banish Big 👍 from me. |
both the "Implementation Switching" and "Mixins" alternatives do not create better code imo.
this would be inordinately tedious and having to reproduce the conditional for every decl would either make copy/pasting it error prone or force it to be a local decl which feels silly.
this is not a valid alternative to one of the main ways i use mixins. consider the following code from a web project im working on: pub const User = struct {
pub const table_name = "users";
pub usingnamespace ox2.sql.TableTypeMixin(User);
id: User.Id = .{ .id = 0 },
uuid: ulid.ULID,
provider: string,
snowflake: string,
name: string,
joined_on: ox2.db.Time,
last_updated: ox2.db.Time,
}; adding an extra field here is not possible because the type is a template for the database table specified by its const FE = std.meta.FieldEnum;
const FT = std.meta.FieldType;
pub fn byKey(alloc: std.mem.Allocator, comptime key: FE(T), value: FT(T, key)) !?T {
pub fn byKeyDesc(alloc: std.mem.Allocator, comptime key: FE(T), value: FT(T, key)) !?T {
pub fn byKeyCount(alloc: std.mem.Allocator, comptime key: FE(T), value: FT(T, key)) !u64 {
pub fn byKeyAnd(alloc: std.mem.Allocator, comptime key: FE(T), value: FT(T, key), comptime key2: FE(T), value2: FT(T, key2)) !?T {
pub fn byKeyAll(alloc: std.mem.Allocator, comptime key: FE(T), value: FT(T, key), comptime ord: Order) ![]T {
pub fn byKeyAndAll(alloc: std.mem.Allocator, comptime key: FE(T), value: FT(T, key), comptime key2: FE(T), value2: FT(T, key2), comptime ord: Order) ![]T {
pub fn update(self: *T, alloc: std.mem.Allocator, comptime key: FE(T), value: FT(T, key)) !void {
pub fn deleteBy(alloc: std.mem.Allocator, comptime key: FE(T), value: FT(T, key)) !void { as i mentioned these methods are shared on every table struct of this kind so the readability is actually enhanced here, the usingnamespace means i dont have to copy/paste a decl definition into every type when a new method is added, and these operations make querying and updates extremely type safe and database backend agnostic. i would make it through if usingnamespace was removed, but as it stands i very much enjoy using it in this way and feel it is a very expressive construct that strikes a balance between power and tedium when used sparingly. |
The condition is usually just something like
All of those functions other than
EDIT: I forgot to mention, you could combine these mechanisms easily: pub const User = struct {
pub const Db = ox2.sql.TableTypeMixin(User, "db", "users"); // btw, pass this name in rather than having a random decl lookup!
db: Db = .{},
// (real fields here)
}; And also, just to be clear, |
i left it out for brevity but in context it is very obvious what this type is for because its accessed as here's some more code that illustrates how its currently used in context: const issueno = std.fmt.parseUnsigned(u64, subpage, 10) catch return fail(response, body_writer, .not_found);
const issue = try db.Issue.byKeyAnd(alloc, .repo, repoo.id, .number, issueno) orelse return fail(response, body_writer, .not_found);
const all_labels = try db.IssueLabel.byKeyAll(alloc, .repo, repoo.id, .asc);
switch (response.request.method) {
.GET => {
const actions = try db.IssueAction.byKeyAll(alloc, .issue, issue.id, .asc);
var name_cache = db.User.IdCache.init(alloc);
const page = files.@"/issue.pek";
const tmpl = comptime pek.parse(page);
try response.headers.append("content-type", "text/html");
try response.headers.append("x-page", "issue");
try pek.compile(@This(), alloc, body_writer, tmpl, .{
//...html template keys
that could work but it adds a layer of indirection that i dont feel adds much value here other than being a clear workaround for the lack of usingnamespace |
I'm in favour of removing usingnamespace. Implementation switching can be even more robust than proposed (in my opinion) by switching on entire structs (each containing an implementation of every function) verses switching per function: Take this example from one of my projects: ///Implementation structure
const Impl = switch (quanta_options.windowing.preferred_backend) {
.wayland => @compileError("Wayland not supported"),
.xcb => @import("xcb/XcbWindow.zig"),
.win32 => @compileError("Win32 not supported"),
};
pub const Window = struct {
//switched out at compile time of course
impl: Impl,
///Some general documentation about the usage of this function
pub fn implFunction(self: Window) void {
return self.impl.implFunction();
}
};
comptime {
//do some validation of implementations maybe
}
test "Maybe even do some general behaviour tests" {
} And if a function isn't defined on the struct, with adequate naming, you get the good error messages which tell you if a field or function isn't defined on an a particular implementation struct. It also means all the implementations are grouped together based on the platform (platforms are just one example but it applies to vector mathematics as well). This is more verbose but it allows you to:
This is my preferred way to do it, but it might be too cumbersome for some people's tastes, which I get. |
I don't have any major complaints againsts removing of System feature detection with This is relevant to removing |
Notably, |
the field can be renamed to make it clearer. in time, we may see a conventional qualifier for this arrangement emerge. |
@notcancername since writing the original proposal, we've eliminated the |
Even if we never call Problem code: Lines 343 to 347 in ca012e5
Do you think To be clear, I'm not advocating for |
@mlugg this ticket might be related ( |
It's not impossible to make this explicit. Here's a variant: obviously modifying, no need for pub fn CounterMixin(comptime T: type, field_name: []const u8) type {
return struct {
parent: *T,
pub fn increment(counter: @This()) void {
@field(counter.parent, field_name) += 1;
}
};
}
const Foo = struct {
field: usize,
pub fn counter(_: Foo, self: *Foo) CounterMixin(@This(), "field") {
return .{ .parent = self };
}
// dunno, this seems enough to me, you couldn't call `increment()` anyway
// if the original wasn't `var`.
// pub fn counter(self: *Foo) CounterMixin(@This(), "field") {
// return .{ .parent = self };
// }
};
pub fn main() !void {
var foo: Foo = .{ .field = 0 };
foo.counter(&foo).increment();
const foo_counter = foo.counter(&foo);
foo_counter.increment();
// const foo_counter = foo.counter();
// foo_counter.increment();
std.debug.assert(foo.field == 2);
}
pub const User = struct {
pub const table_name = "users";
// pub usingnamespace ox2.sql.TableTypeMixin(User);
id: User.Id = .{ .id = 0 },
uuid: ulid.ULID,
provider: string,
snowflake: string,
name: string,
joined_on: ox2.db.Time,
last_updated: ox2.db.Time,
// is this not enough?
pub fn as_table(self: *User) ox2.sql.TableTypeMixin(User) {
return .{ .parent = self };
}
}; |
I've been using zigwin32 lately and I was wondering how I would do this without const win32 = struct {
usingnamespace @import("win32").zig;
usingnamespace @import("win32").foundation;
usingnamespace @import("win32").system.memory;
usingnamespace @import("win32").ui.windows_and_messaging;
usingnamespace @import("win32").graphics;
usingnamespace @import("win32").graphics.gdi;
}; Exemple from zigwin32: https://github.com/marlersoft/zigwin32gen/blob/main/examples/helloworld-window.zig#L1-L14 |
It seems like your code snippet and the win32 module disagree on the proper fully qualified namespaces for those symbols. I think you should just do |
there already exists |
yes, but it's generated. so the real question is... given that this is valid use-case (because people sometimes want to use flat namespaces and some other people are willing to provide it, like another interesting example is
I think the argument for fully-qualified namespaces completely fades in the context of writing tests, because obviously BTW: I am not the only one doing the EDIT: just to be clear, this is not about defending |
The case of merging multiple namespaces into one is also much less readable without usingnamespace than it is with -- there's simply no easy way to tell where all the decls come from. Removing usingnamespace is more of a necessary evil to make the compiler simpler. |
this seems reasonable to me, but i'd name the second one |
https://ziglang.org/documentation/0.13.0/#Avoid-Redundancy-in-Names
|
I don't have any particular opinion on removing fn FuncList(comptime T: type) type {
return struct {
pub const items = .{
struct {
pub fn funcA(self: *T) void{}
},
struct {
pub fn funcB(self: *T) void{}
},
struct {
pub fn funcC(self: *T) void{}
},
};
};
}
fn MergeStruct(comptime A: type, comptime B: type) type {
return struct {
pub usingnamespace A;
pub usingnamespace B;
};
}
fn Methods(comptime List: type) type {
comptime var ret = struct{};
inline for (List.items)|item| {
ret = MergeStruct(ret, item);
}
return ret;
}
const MyStruct = struct {
pub usingnamespace Methods(FuncList(@This()));
}
// now MyStruct have funcA/B/C does anyone have a good solution for this problem?
|
I heavily use usingnamespace for Mixins. It's very convenient and also readable in my opinion. I don't like the suggested alternative for Mixins very much because its lacks readability and one would have to change a lot on the caller site as well as on the receiver side. This would lied to more boilerplate code as well. If usingnamespace gets removed I hope at least there will be a good alternative for Mixins. Since Mixins are a well known concept I suggest just that:
|
@AndreasHefti that isn't a working proposal, you've literally just replaced the Mixins are fairly rare in Zig, and so are not something which I think can be reasonably used as a justification for a complex language feature. I remain convinced that there need be no language-level replacement. Most places where you could use a mixin are better served by other structures, and for the few remaining cases where mixins genuinely could be useful (such as refcounting), the solution in the original issue post works fine. |
@mlugg Thanks for the info. I already working on replacing |
Hi, I wanted to mention a possible usecase that I had for usingnamespace that I couldn't replace with the above methods. However, I think it would be unnecessary if @type{ .Struct } could set .decl . I was considering a MMIO using the method.
And although I could add a 0 width element called mmio. The addresses of a packed bit type have a different quality, and I am not really wanting to make extra name that is common across all mmio types so that I could get the first field in the parent. But then again maybe there is a way... I just wanted to present the use case. |
Talking about mixins, I wonder how would one express something like this |
If this ever go away, then please let us group imports this way: const all = {
@import("a");
@import("b");
@import("c");
}; I like to split my files so they are not bunch of un-readable/un-navigable 50k LOC files |
@bangbangsheshotmedown what you wrote is also purely a syntax change and does not address the proposal |
@nektro I think the point was that people want ability to import everything from this, everything from that, and keep it together in one (flat) namespace, which is what usingnamespace does currently. |
This is a proposal to eliminate the
usingnamespace
keyword from the Zig programming language. I know this is a controversial one, but please read the whole thing before leaving any comments.Overview
The
usingnamespace
keyword has existed in Zig for many years. Its functionality is to import all declarations from one namespace into another.usingnamespace
was formerly calleduse
, and was actually considered for deletion in the past -- see #2014. Ultimately, Andrew decided not to remove it from the language, with the following main justifications:pub usingnamespace @cImport(@cInclude(...))
is a helpful pattern. This point is made obsolete by move@cImport
to the build system #20630.usingnamespace
. These uses have been all but eliminated -- this is discussed below.In recent years, the usage of
usingnamespace
in first-party ZSF code has declined. Across the entire compiler, standard library, and compiler-rt, there is now precisely one usage ofusingnamespace
:zig/lib/std/c.zig
Lines 35 to 48 in 9d9b5a1
Every other
grep
result that shows up forusingnamespace
is simply supporting code for it across the compiler pipeline. This leads us to revisit the decision to keep it in the language.This is a proposal to delete
usingnamespace
from the language, with no direct replacement.Why?
This proposal is not just to prune language complexity (although that is one motivation). In fact, there are three practical reasons I believe this language feature should be deleted.
Code Readability
A core tenet of Zig's design is that readability is favored over writability, because the majority of any programmer's time is inevitably spent reading code rather than writing it. Unfortunately,
usingnamespace
has a tendency to significantly harm the readability of code that uses it.Consider, as an example, the singular usage of
usingnamespace
in the standard library. If I want to call the Linux libc functiondl_iterate_phdr
, where should I look to determine if this function is instd.c
? The obvious answer would bestd/c.zig
, but this is incorrect. Theusingnamespace
here means that the definition of this function is actually instd/c/linux.zig
! Similarly, if I want to discover which platforms this function is defined on, and how its signature differs between platforms, I have to check each file instd/c/*.zig
, and compare the signatures between those files.Without
usingnamespace
, the story would be completely different.dl_iterate_phdr
would be defined directly instd/c.zig
, perhaps with aswitch
per-target. All of the information about this function would be localized to the namespace which actually contains it: I could see, in the space of perhaps 10 lines, which platforms this function was defined on, and whether/how the signatures differ.This is one example of a more general problem with
usingnamespace
: it adds distance between the "expected" definition of a declaration and its "actual" definition. Withoutusingnamespace
, discovering a declaration's definition site is incredibly simple: you find the definition of the namespace you are looking in (for instance, you determine thatstd.c
is defined bystd/c.zig
), and you find the identifier being defined within that type declaration. Withusingnamespace
, however, you can be led on a wild goose chase through different types and files.Not only does this harm readability for humans, but it is also problematic for tooling; for instance, Autodoc cannot reasonably see through non-trivial uses of
usingnamespace
(try looking fordl_iterate_phdr
understd.c
in the std documentation).Poor Namespacing
usingnamespace
can encourage questionable namespacing. When declarations are stored in a separate file, that typically means they share something in common which is not shared with the contents of another file. As such, it is likely a very reasonable choice to actually expose the contents of that file via a separate namespace, rather than including them in a more general parent namespace. I often summarize this point as "Namespaces are good, actually".For an example of this, consider
std.os.linux.IoUring
. In Zig 0.11.0, this type (at the time namedIO_Uring
) lived in a filestd/os/linux/io_uring.zig
, alongside many "sibling" types, such asSubmissionQueue
. This file was imported intostd.os.linux
with apub usingnamespace
, resulting in namespacing likestd.os.linux.SubmissionQueue
. However, sinceSubmissionQueue
is directly related to io_uring, this namespacing makes no sense! Instead,SubmissionQueue
should indeed be namespaced within a namespace specific to io_uring. As it happens, this namespace is, well, theIoUring
type. We now havestd.os.linux.IoUring.SubmissionQueue
, which is unambiguously better namespacing.Incremental Compilation
A key feature of the Zig compiler, which is rapidly approaching usability, is incremental compilation: the ability for the compiler to determine which parts of a Zig program have changed, and recompile only the necessary code.
As a part of this, we must model "dependencies" between declarations in Zig code. For instance, when you write an identifier which refers to a container-level declaration, a dependency is registered so that if that declaration's type or value changes, this declaration must also be recompiled.
I don't intend to explain the whole dependency model in the compiler here, but suffice to say, there are some complexities. One complexity which is currently unsolved is
usingnamespace
. The current (WIP) implementation of incremental compilation essentially assumes thatusingnamespace
is never used; it is not modeled in the dependency system. The reason for this is that it complicates the model for the reasons we identified above: withoutusingnamespace
, we can know all names within a namespace purely from syntax, whereas withusingnamespace
, semantic analysis may be required. The Zig language has some slightly subtle rules about whenusingnamespace
declarations are semantically analyzed, which aims to reduce dependency loops. Chances are you have never thought about this -- that's the goal of those rules! However, they very much exist, and modelling them correctly in incremental compilation -- especially without performing a large amount of unnecessary re-analysis -- is a difficult problem. It's absolutely a surmountable one, but it may be preferable to simplify the language so that this complexity no longer exists.Note that changing the language to aid incremental compilation, even if the changes are not strictly necessary, is something Andrew has explicitly stated he is happy to do. There is precedent for this in, for example, the recent changes to type resolution, which changed the language (by introducing the rule that all referenced types are fully resolved by the end of compilation) to simplify the implementation of incremental compilation.
Use Cases
This section addresses some common use cases of
usingnamespace
, and discusses alternative methods to achieve the same result without the use of this language feature.Conditional Inclusion
usingnamespace
can be used to conditionally include a declaration as follows:The solution here is pretty simple: usually, you can just include the declaration unconditionally. Zig's lazy compilation means that it will not be analyzed unless referenced, so there are no problems!
Occasionally, this is not a good solution, as it lacks safety. Perhaps analyzing
foo
will always work, but will only give a meaningful result ifhave_foo
is true, and it would be a bug to use it in any other case. In such cases, the declaration can be conditionally made a compile error:Note that this does break feature detection with
@hasDecl
. However, feature detection through this mechanism is discouraged anyway, as it is very prone to typos and bitrotting.Implementation Switching
A close cousin of conditional inclusion,
usingnamespace
can also be used to select from multiple implementations of a declaration at comptime:The alternative to this is incredibly simple, and in fact, results in obviously better code: just make the definition itself a conditional.
Mixins
Okay, now we're getting to the big one. A very common use case for
usingnamespace
in the wild -- perhaps the most common use case -- is to implement mixins.Obviously this simple example is a little silly, but the use case is legitimate: mixins can be a useful concept and are used by some major projects such as TigerBeetle.
The alternative for this makes a key observation which I already mentioned above: namespacing is good, actually. The same logic can be applied to mixins. The word "counter" in
incrementCounter
andresetCounter
already kind of is a namespace in spirit -- it's like how we used to havestd.ChildProcess
but have since renamed it tostd.process.Child
. The same idea can be applied here: what if instead offoo.incrementCounter()
, you calledfoo.counter.increment()
?This can be elegantly achieved using a zero-bit field and
@fieldParentPtr
. Here is the above example ported to use this mechanism:This code provides identical effects, but with a usage of
foo.counter.increment()
rather thanfoo.incrementCounter()
. We have applied namespacing to our mixin using zero-bit fields. In fact, this mechanism is more useful, because it allows you to also include fields! For instance, in this case, we could move the_counter
field toCounterMixin
. Of course, in this case that wouldn't be a mixin at all, but there are certainly cases where a mixin might require certain additional state, which this system allows you to avoid duplicating at the mixin's use site.External Projects
This section will look over uses of
usingnamespace
in some real-world projects and propose alternative schemes to achieve the same effect.Mach
I have discussed Mach's usages of
usingnamespace
with @slimsag in the past, and we agreed on migration mechanisms, so I won't go into too much detail here. A quick summary of a few of them:math/ray.zig
: this is redundant; the error case is already caught above.math/mat.zig
: this logic can be mostly generalized with some simple metaprogramming. What remains can be handled by the "implementation switching" approach discussed above or similar.math/vec.zig
: same asmat.zig
.sysaudio/wasapi/win32.zig
: these are a little awkward for ABI reasons, but this is generated code so it's fine if it ends up a little verbose, and most of it is unused IIRC.TigerBeetle
node.zig
: this code usesusingnamespace
to merge two namespaces. Probably these should just be imported separately, undertb
andtb_client
respectively.testing/marks.zig
: re-expose the 4 functions fromstd.log.scoped
, and conditionally definemark.err
etc as needed.vsr/message_header.zig
: namespaced mixins, as described above.flags.zig
: definemain
conditionally, as described above.ring_buffer.zig
: implementation switching, as described above.tracer.zig
: implementation switching, as described above.lsm/segmented_array.zig
: conditional inclusion, as described above.clients/java/src/jni.zig
: replaceusingnamespace JniInterface(JavaVM)
withconst Interface = JniInterface(JavaVM)
, and replaceJavaVM.interface_call
withInterface.call
(renaming that function).clients/node/src/c.zig
: to be replaced with build system C translation, see move@cImport
to the build system #20630.ghostty
I won't explain any specific cases here, since ghostty is currently in closed beta. However, I have glanced through its usages of
usingnamespace
, and it appears most could be eliminated with a combination of (a) more namespacing and (b) occasional manual re-exports.The text was updated successfully, but these errors were encountered: