-
-
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
replace anytype #17198
Comments
fn doSomethingWithAStringAndANumber(str: []u8, n: unsigned8BitOr16Bit) void {
// use n. its either a u8 or a u16!
}
Edit: outdated comment |
@zzyxyzz I have edited the original issue so now its almost totally unrecognizable. Sorry to make you read the scrapped original version, but at the time I didn't know about all the issues before me that had tried to do the same thing. In fact, in some cases, the exact same thing. GitHub's search feature is a bit of a joke if you don't know the exact name of the issue you're looking for. So thanks for linking that issue, I very well might not have found it otherwise. The new version of this issue is more of an editorial summary of the state of |
@the-argus, However, at this point, the ship has sailed for any such radical redesign of the type system. FYI, the official position of the core team is that the language is mostly done and most open proposals will be rejected. As for the existing antype-style generics, a lot of people have thought long and hard about how to improve them, but without too much success. Procedural generics are kind of contagious, and it's really hard to carve out a declarative subset that is 1) general enough 2) will not cause problems elsewhere and 3) offers substantial benefits over a manual type assertion at the top of the function. My own proposal (#9260) is one of the last ones open, and it is ultimately fairly superficial. In short, I would not get my hopes up high that there will be a substantial paradigm shift in Zig's generics. They are as powerful as they are unergonomic, but it is genuinely difficult to shift this tradeoff. Since you're not actually proposing any solution, I'd recommend closing this issue. P.S.: Concerning the |
Okay, but... that's not what we want. The reason
To be clear, none of this is really a problem of implementational complexity - any of the ideas talked about here could be integrated into the compiler fairly trivially. The problem is not that we don't want to bother solving the problem in the compiler, but instead that we don't necessarily think any such idea is a good fit for Zig as a language.
I don't think this is really anyone's position on the matter, at least not to this extreme. It's not controversial to say that generic code can be useful, and often clearly the right tool for the job. The danger is that if Zig has language-level support for more complicated generic patterns - the kind of thing that requires a lot of complex validation logic - then we implicitly encourage such constructs, which often results in a web of unnecessary abstraction - look at the modern C++ standard library for instance. Status quo Zig allows these patterns, and allows you to easily integrate validation logic when you need (see the
I'd just like to close out by saying that as someone who's been using Zig as their primary language for a few years now, I've never actually had any major issues with (As a note, I do remain in support of the |
I think it is odd that
From // If you get a compile error on this line, it means that your generic eql
// function is invalid for these parameters.
const eql = ctx.eql(key, test_key.*); Is this really... good? bits of the type validation end up buried in the code. What if you could just go-to-definition on the
I think that
This is true! And to allow for specifying such complex constraints, we should not create a complex type declaration syntax. And then make it accidentally turing complete. And then make that a feature. That would be bad. Instead, we could just... put the type constraining code in a block and give it a helpful name. |
This issue was sort of a cry for help... I know proposals are not being accepted but to me generics feel like a huge hole in what is otherwise an awesome language. I just want perfection, is that too much to ask? /s
Reminds me of C++.
Yeah... all I've got is the suggestion to test all type combinations. I'm going to leave it open for a day or so but then I will close it based on your input.
yay :) |
You can avoid Then, you have to use |
I see you have some actual experience using zig in earnest so it's ok 👍 |
@andrewrk Can you update the issue template to clarify this? I was banned once so it seemed arbitrary. |
Thanks for providing a nice history/summary! Let me restate the fundamental problem to set the stage for an idea:
I would categorize the following as symptoms of this fundamental problem:
So far the ideas we've seen give the developer additional tools they can use to help alleviate the fundamental problem. However, I wonder if we could improve the situation without requiring any new langauge syntax nor any extra specification from library authors? It seems to me there's already alot of information about an fn example(c: anytype) void {
c.foo();
} It's clear that // call the example above
example(42);
// zig: error: function `example` requires a type that has a function 'foo' but you provided comptime_int This could also be used in doc generation:
Now, this is a very simple example. Deriving a contract for a type based on code is hard to generalize I think. Take this example: fn example(c: anytype) void {
if (some_condition) {
return c.foo();
} else {
return c.bar();
}
} Now what is the compiler to do? I think this is a bit of an unexplored space, but what's nice is if we can find improvements here, you make the lives of all Zig developers better at no cost to them, no new language syntax to learn/use. Also keep in mind that if we don't explore this direction, then any new feature that we add to the language to alleviate the problem may actually be rendered unnecessary because the code was already giving the compiler all the information it needed. |
@marler8997, This is a problem shared by all languages that do not have formal interfaces and use some form of duck typing for polymorphism (Python, Julia, C++ templates, etc.): faults can only be consistently detected for concrete instantiations, so you often have an error bubbling up from somewhere deep in the call stack and have no help in diagnosing the actual problem. Zig is actually not the worst offender in this regard, because building very many layered comptime abstractions is not idiomatic, but the basic problem is the same. The only real countermeasure is to provide detailed (and up-to-date) documentation and manually insert comptime checks. |
I see the concerns but I have a feeling we could do a lot more here than it may seem at first. For example, nested functions doesn't seem like an issue, the caller simply inherits the contract established in the callee. It's hard to say where the real pain points will be so let's explore. Let's take a look at pub fn len(value: anytype) usize {
switch (@typeInfo(@TypeOf(value))) {
.Pointer => |info| switch (info.size) {
.Many => {
const sentinel_ptr = info.sentinel orelse
@compileError("invalid type given to std.mem.len: " ++ @typeName(@TypeOf(value)));
const sentinel = @ptrCast(*align(1) const info.child, sentinel_ptr).*;
return indexOfSentinel(info.child, sentinel, value);
},
.C => {
assert(value != null);
return indexOfSentinel(info.child, 0, value);
},
else => @compileError("invalid type given to std.mem.len: " ++ @typeName(@TypeOf(value))),
},
else => @compileError("invalid type given to std.mem.len: " ++ @typeName(@TypeOf(value))),
}
} The way I imagine this working is that we analyze the We start our "Generic Type Analysis" pass of the switch (@typeInfo(@TypeOf(value))) Since The same is done for So next the analyzer sees that we call
Keep in mind that with this sort of analysis, pub fn len(value: anytype) usize {
switch (@typeInfo(@TypeOf(value))) {
.Pointer => {
// ...
},
else => @compileError(), // no need for message!
}
} I don't know about you but this alone already seems promising enough to explore. Even if it's not possible to solve this in every case, it could make using the language greatly improved I think. |
Some afterthoughts. Note that since this idea adds an "analysis pass" on every generic function, we can not only detect issues with the caller types, we can also detect errors in the function body regardless of the parameter types passed in.
In this example, when zig does its "analysis pass", it sees that Also to add to my previous comment. Capturing type constraints like I outlined above would explode if you try to perfectly capture all the specific cases code operations can affect a type, but, keep in mind that this analysis doesn't have to be perfect. The type constraints don't affect compilation, they only affect error messages and documentation. So maybe you have some sort of nested branching code that's 10 levels deep based on the type of a value, so you end up with thousands of possible type combinations depending upon the input, but we don't need to worry about perfectly tracking all those combinations. Instead we can reduce them to a flat set of valid/invalid general types. For example, maybe the type of Here's a shorter way of saying the same thing: We don't need to know exactly WHEN an anytype can or can't be something...we only need to know IF it can. I think not having to know the WHEN might be the key to preventing this from exploding. |
Afterthoughts Part 2. First, note that this "type analysis pass" also applies to any function that accepts a Second, if Zig employed this "type analysis pass", we could introduce a new builtin pub fn len(value: anytype) usize {
switch (@typeInfo(@TypeOf(value))) {
.Pointer => |info| switch (info.size) {
.Many => {
const sentinel_ptr = info.sentinel orelse @genericTypeError(),
const sentinel = @ptrCast(*align(1) const info.child, sentinel_ptr).*;
return indexOfSentinel(info.child, sentinel, value);
},
.C => {
assert(value != null);
return indexOfSentinel(info.child, 0, value);
},
else => @genericTypeError(),
},
else => @genericTypeError(),
}
} This gives the programmer some control over making sure that Zig is able to make sense of what their code is doing. If for example you called pub fn example() void {
@genericTypeError();
}
// error: misuse of `@genericTypeError`, there are no generic types in the current context Or if you're inside a generic function and call pub fn example(c: anytype) void {
@genericTypeError();
}
// error: misuse of `@genericTypeError`, all generic types `c` are currently outside any conditional |
@marler8997 thanks for giving me hope that this experience can be improved! This is an exciting potential solution and I think this level of introspection from tooling should be considered required by any language with duck typing elements. I know the pyright LSP implementation is capable of doing stuff like this, and it can be very nice to work with. Other languages with duck typing and no tooling for it (C++ and clangd, zig and zls) really make it a pain. Particularly when you call or write a generic function and then immediately lose all completion functionality. I think the analysis you're describing generalizes to namespace resolution for generic types- the language server can autocomplete fields and functions which exist in the contract. Maybe only to a certain nested function call depth depending on the exploding you mentioned. |
Hey there I'm reaching out as I'm currently learning Zig and making some investigations into runtime polymorphism. While diving into Zig, one thing that's been bugging me is the lack of convenient built-in compile-time polymorphism. and force me to use So after reading some discussions and articles around this topic, I want to suggest a type of constraint mechanism that looks to me as native to the language (just compile time checking step) Current code: const std = @import("std");
fn main() void {
const some1 = SomeStruct{ .data = 42 };
const some2 = SomeAntherStruct{ .data = [_]u8{1,2,3,4} };
print(some1);
print(some2);
}
fn print(f: anytype ) void {
f.x();
}
// return type is the same as the parameter type
fn printAndReturn(f: anytype ) @Type(f) {
f.x();
return f;
}
const SomeStruct = struct {
const Self = @This();
data: u8,
fn print(self: Self) void {
std.debug.print("data = {}\n", .{self.data});
}
};
const SomeAntherStruct = struct {
const Self = @This();
data: [4]u8,
fn print(self: Self) void {
std.debug.print("data = {}\n", .{self.data});
}
}; Suggested: const std = @import("std");
fn main() void {
const some1 = SomeStruct{ .data = 42 };
const some2 = SomeAntherStruct{ .data = [_]u8{1,2,3,4} };
print(some1);
print(some2);
}
// ~ means type constraints - in this case it must contain the Printer methods
fn print(f: ~Printer ) void {
f.x();
}
// return type is the same as the parameter type
fn printAndReturn(f: ~Printer ) @Type(f) {
f.x();
return f;
}
// kind of interface/trait
const Printer = struct {
const Self = @This();
// use ~ again for self type
print: fn(self: ~Self) void,
};
const SomeStruct = struct {
const Self = @This();
data: u8,
fn print(self: Self) void {
std.debug.print("data = {}\n", .{self.data});
}
};
const SomeAntherStruct = struct {
const Self = @This();
data: [4]u8,
fn print(self: Self) void {
std.debug.print("data = {}\n", .{self.data});
}
}; The proposed changes introduce type constraints using the ~ symbol, allowing developers to specify required methods that a parameter must implement at compile time. This would solve these problems mentioned by @marler8997
Node: |
I would also prefer the sort of syntax proposed by @moshe-kabala so as to formally document type constraints on generic input parameters However I think, this sort of My own two cents is that it seems much better for there to be specific syntax for when And that instead of anytype, something along the lines of this could be done pub const Foo = struct {
a: u32 = 0,
b: f32 = 0,
pub fn print(self: *@This()) void {
...
}
}
pub fn doSomething(printable: @hasDecl("print")) void {
foo.print();
...
}
const foo = Foo{};
doSomething(foo);
But something along the lines of this might help clarify what exactly the |
@moshe-kabala As VisionDev said, stuff like what youre describing has already been rejected, with the explanation that I quoted in the original post. #1669 in particular is similar to what you've proposed. There have been multiple other similar proposals closed for the same reason, such as the one proposing @trait which I linked in the original post. |
@VisenDev I think this is promising! const MyConstraint = @decls({
@hasDecl("read"),
@hasDecl("write"),
@hasDecl("print"),
});
pub fn doSomething(readWritePrintable: MyConstraint) void {
readWritePrintable.print();
...
} And this is interesting. Only being able to ask for decls and not specific function interfaces reduces complexity. But I think this is the worst of both worlds, not the best of both worlds. The type returned by your proposed I think you ultimately you get all the problems of adding |
Yes, that is a fair point Another idea would be to add a constraint which ensures an input type is derived from a specific generic pub fn Writer(T) type {
return struct {
pub fn print() void {
...
}
};
}
pub fn doSomething(foo: Writer(anytype)){
....
} The syntax is just a placeholder, but the idea is that you ensure that the anytype is derived from a specific generic function Other syntax: pub fn doSomething(foo: @any(Writer)){}
pub fn doSomething(foo: Writer(comptime T: type)){} |
@the-argus, @VisenDev Edit: oh, I see that #6615 is already referenced in the top post; sorry. |
Yeah, @VisenDev I don't think there's much to be thought about here that hasn't already been thought about. Marler's idea is the most novel and really the thing keeping me from closing this issue. I feel pretty certain that there is no language feature we could add which would solve this problem and also keep everyone happy. |
Thats fair, I guess we just have to live with the current situation It seems the main issue here is the difficulty in creating the proper tooling to accompany the language, not the language design itself |
If Zig is extended to allow comptime struct fields without default values and referring to previous comptime struct fields (e.g. via a builtin that allows access to previous comptime fields of the current struct, which I'll call pub const AnyType = packed struct {
comptime T: type,
value: @thisPtr().T,
}; This doesn't address your discontent with |
comptime fields afaik are essentially a differently-accessed decl and have the same value for all instances of the type. so generics and ducktyping are still the only way |
In that case, what about the following that returns fresh instantiations of the type? pub fn AnyType() type {
return packed struct {
comptime T: type,
value: @thisPtr().T,
};
} The important thing here is delaying resolution of the value of the field const std = @import("std");
pub const ComptimeAnyTypeConstPtr = struct {
T: type,
ptr: *const anyopaque,
pub fn dereferencePtrType(comptime PtrT: type) type {
return switch (@typeInfo(PtrT)) {
.Pointer => |ptrInfo| ptrInfo.child,
else => @compileError("ComptimeAnyTypeConstPtr ptr must be a pointer"),
};
}
pub fn init(comptime ptr: anytype) ComptimeAnyTypeConstPtr {
const T: type = dereferencePtrType(comptime @TypeOf(ptr));
return .{ .T = T, .ptr = @as(*const anyopaque, @ptrCast(ptr)) };
}
pub fn get(comptime self: *const ComptimeAnyTypeConstPtr) *const self.T {
return @as(*const self.T, @alignCast(@ptrCast(self.ptr)));
}
pub fn set(comptime self: *ComptimeAnyTypeConstPtr, ptr: *const self.T) void {
self.ptr = @as(*const anyopaque, @ptrCast(ptr));
}
};
pub fn main() !void {
const str: *const []const u8 = comptime blk: {
var ptr = ComptimeAnyTypeConstPtr.init(&@as([]const u8, "base"));
ptr.set(&@as([]const u8, "codebase"));
break :blk ptr.get();
};
std.debug.print("All your {s} are belong to us.\n", .{str.*});
} The big restriction here is that instances of |
i think related but other proposals are closed, is this project of interest https://github.com/permutationlock/zimpl |
I find this quote very interesting because it is conflating two very distinct things: making generics easier to implement, and making generics easier to use. I agree that generic code is hard. Hard to implement correctly, hard to test, hard to use, hard to reason about. But, for better or worse, Zig has generics. That is something that cannot be ignored. The presence of generic capabilities means that generic code will be written; most of the std relies on generic code. Andrew once described the idea of friction:
Following this train of thought, I agree that, if generics are hard to get right, that there should be friction to implementing generic code. By making the implementation of generics this easy, all of the reasoning overhead is pushed down onto the consumers of our code: developers using the std, developers using zig libraries. What these proposals are attempting to do is to is to push that friction back to the implementors. It is not making generics easier to implement, it's adding gotchas and checks that can be used to verify our own generic code while at the same time informing our users about our expectations, so that they can have an easier time, instead of us. Zig already supports expressions as types, thanks to comptime evaluation. It stands to reason, to me, to allow pushing that one step further and either allow comptime function predicates to be passed in place of types, to be evaluated and checked on compilation and to inform useds via readable names and centralized logic what a given function expects them to provide as an argument. This would be equivalent to this proposal pub fn fooable(comptime T: type) bool { ... }
pub fn foo(value: fooable) void { ... } I've also seen the following alternative that is essentially equivalent pub fn fooable(comptime T: type) type {
// --- comptime checks ---
return T;
}
pub fn foo(value: fooable(@TypeOf(value))) void { ... } But to me this is just a roundabout way of doing predicates that is harder to read for little gain. An argument could be made that additional arguments can be passed to the function but this can also be achieved with the predicate option if we treat it as a function pointer pub fn sizedFooable(size: u8) fn(comptime T: type) bool {
return struct {
const inner_size: u8 = size;
pub fn fooable(comptime T: type) bool { ... }
}.fooable;
}
pub fn foo(value: sizedFooable(4)) void { ... } It's more of a hassle to write but that aligns with the idea of friction for the implementer. Meanwhile, the user gets to reap the benefits by getting a readable type constraint whose specific constraints are contained in a centralized place. |
I like the route that @moshe-kabala is suggesting though might i suggest the name "shape" which is effectively just a set of expectations that a given type should meet during compile time. Then the $ token can be used instead of "~" Additionally i've added the ability to construct sets of shapes similar to how error types work currently. const std = @import("std");
fn main() void {
const some1 = SomeStruct{ .data = 42 };
const some2 = SomeAntherStruct{ .data = [_]u8{1,2,3,4} };
print(some1);
print(some2);
}
fn print(comptime f: $Printer ) void {
f.print();
}
fn printAndSend(comptime f: $SendPrinter) void {
f.print();
f.send();
}
// A "shape" is a set of expectations a given type must fulfill at compile time
const Printer = shape {
const Self = @This();
fn print(self: $Self) void;
};
const Sender = shape {
const Self = @This();
fn send(self: $Self) void;
};
const SendPrinter = $Printer || $Sender; // Merge multiple shapes into a set similar to error sets
const SomeStruct = struct {
const Self = @This();
data: u8,
fn print(self: Self) void {
std.debug.print("data = {}\n", .{self.data});
}
fn send(self: Self) void {
// send over a socket
}
};
const SomeAntherStruct = struct {
const Self = @This();
data: [4]u8,
fn print(self: Self) void {
std.debug.print("data = {}\n", .{self.data});
}
}; Additionally this comment to me best describes the general issue here and why this is so much needed in the language: TLDR: It's the fact that duck typing makes knowing the expectations of a set of functions extremely difficult for more complex codebases. Declaring those expectations up front using this type of "Shape" based system resolves the issues i see with the current system. Also note: This is something that does not solve the "problem" of multiple concrete types being declared as parameters of a function. It's explicitly around declaring the "shape" of structs during compile time, basically a more explicit form of duck typing. edit: |
Full disclosure, I haven't read all the replies yet. So, if this was covered... oops. But, when designing a language (or anything really) there are two camps generically speaking: Camp 1 Those who don't want change... much. These people who came from C, etc. "We did it this way for years and that's just the way it is." Camp 2 Those who don't want change... :). These people may have come from a higher level language and are used to newer concepts like generics for instance. To be completely objective, you have to ask yourself who's right and who's wrong here? And also, who's the target audience(s) and what is the goal of Zig? If the goal of Zig is to be "C evolved" and low level enough for C devs but modern and friendly enough for non C devs, then it behooves us to think not only like a C dev but also like a modern dev. To bring it back to generics, generics help reduce code complexity and promote code re-usability. Literally just about every modern language supports them. And the compiler is smart enough to figure out the typing. Can they be abused? Sure. But so can pointers. This is where the artistic side of coding comes in. You had mentioned that type inference may be hard on the developer, but as long as Zig provides a way to have type constraints and the compiler errors as a result of a generic not meeting that type, the developer using such a routine will very soon figure out type A or type B is not allowed. In closing, I'll just say... if Zig is just a renamed C then there's less incentive to use Zig and just stick with C. So we need to meet in the middle between old skool and modern. |
EDIT: this issue is about both
anytype
andcomptime T: type
. All generics in zig, and how the types are constrained. for the most part, when I sayanytype
, just think "generics".anytype
is a tool to defer all type logic into the function, where it can be dealt with imperatively at compile time. The result is a much poorer experience with language servers and poor ordering of information: the type constraints of the arguments of a function are the first thing you want to know. And yetanytype
leaves them for later.The frustration of wanting to know what a function does and being greeted with
args: anytype
is what this issue is about. It sucks, and I know zig can do better. I'm going to try to summarize some previous issues on the subject, and offer some suggestions/proposals, but I don't want this issue to be closed if a suggestion is rejected, because the point of this issue is discussion, not proposal. If people like the idea then someone can make another issue marked as proposal. This issue will be closed if it's deemed not an issue but a feature, or if all solutions are out of scope.Example
Bad example which was originally in the issue
`std.Build.dependency` is a nightmare. The function signature is `pub fn dependency(b: *Build, name: []const u8, args: anytype) *Dependency` and it takes three go-to-definitions to eventually arrive at `std.Build.applyArgs` where we can actually learn what `args` does. There are no documentation comments above any of these stdlib functions. Perhaps this could be solved by using an options struct of some kind. But nonetheless `anytype` is what got used and it's a problem that zig made it available for use in place of other options.Ideally, we'd like some sort of annotation in the function signature of
dependency
which makes it clear thatargs
should contain a target and user input options. And give a helpful error message when it doesn't, checking at the call site ofdependency
instead of three functions down inapplyArgs
.EDIT: the above example is a bad one, as pointed out here. Consider instead this snippet from the HashMap implementation:
Where
ctx
is a generic type. This constraint is quite difficult to find if you don't find it the hard way, as it is buried in the private API of the HashMap. I think the use ofanytype
in the HashMap implementation is smart but it suffers in readability because there is no separation between comptime type validation logic and runtime logic (the former of which is much more important when learning the API). I think the most conservative solution to this problem would be if zig had some way of annotating a block as "for the type validation logic". Then some comptime asserts with nice error messages or comments could replace the example seen above.Precedent
First, let's take a look at an existing solution, Zig compile-time contracts. We know type constraints can be accomplished in the language already (and that's part of why a proposed solution needs to be really good) so it's worth looking at how it would be implemented given the status quo now. A sample from zig-ctc readme:
Compile time logic has to be inserted as a weird wrapper function around the return type. This could be helped a bit with
infer T
from #9260, but it still ends up pretty weird to read. This feels like something that should be a native language feature. It could be a stdlib feature instead, and maybe it should be.In #1669, a highly related issue, a user proposed using comptime functions which return bool as types. It's readable, offers a nice experience to a user with an editor (they go-to-definition on the type, see instead it's a function, read the logic). Maybe not so nice for someone reading the code on a webpage. And, if #9260 were to be added to the language, we could remove the implicit function call and get the type of the thing in scope of the function signature:
An improvement which I think is worth considering, in the spirit of rethinking these old, still relevant issues. But it doesn't solve the real problem, as we'll see in a moment.
Another, extremely related proposal is #6615, which proposed something like this:
Additionally there were #7232 and #8008 (the latter of which was quite well thought out, proposing an
@trait
builtin. However. all these proposals were ultimately rejected:Here we come to understand that Zig's fundamental focus on clarity conflicts with generics, not that there is some problem with the proposal, really. I said the earlier example of zig-ctc felt like "should be a native language feature." I thought "hey, with some added features to Zig, this could be much more readable, and maybe also in the stdlib." But it's clear now that the zig maintainers don't want
to solve that problem in the compilerto have to add a bunch of complexity to zig only to get a slightly more readable version of the exact same functionality. However,anytype
is still in the language and it still is used for things other than anonymous tuples and it still sucks.Distinct types offer a partial solution
A big part of the goal here is just to provide more readable function signatures. Yes, it would be nice to do checking in the function signature, but maybe it's enough to just provide aliases for anytype? Like a slightly better doc comment. Maybe
anytype
could be replaced by something likeanontuple
and then only allowanytype
when defining type aliases. Consider #5132 (typedef for zig) and #1595 (distinct types).Do something!
anytype
is a problem when used for anything other than anonymous tuples in string formatting. Is the best answer the zig language can give doc comments? We need to come up with some functionality which actually improves the experience of writing type-constrained generic code. Something that solves problems like the type checking code buried inapplyArgs
.Conclusion/Suggestion
It doesn't make sense to say "generic code is bad, its hard to reason about, so we will not add facilities to make it easier because we don't want to encourage its use" when
anytype
still exists and still gets used. It seems to me that people would much rather write generic code with bad facilities likeanytype
and deferred type-constraining logic than maintain multiple well-tested but highly duplicated implementations of the same thing. Anything short of removingcomptime T: type
andanytype
seems unable to stop this.Maybe the reasons we have for
anytype
being bad are enough to warrant reconsidering the previously closed issues like #1669? The fact that constraints onanytype
are evaluated later, and not at the function where they're passed in, leads to confusing error messages. Additionally, it makes the function signature harder to read, and obfuscates information from tooling like ZLS.My only original suggestion is that Zig could automatically generate tests for functions with
anytype
orcomptime T: type
or equivalent. These tests would only test compilation of the function with all types, not actual functionality. It would require some new builtin@constrainedTypeCompileError
or something along those lines, to skip tests for types you know shouldn't work with the function. Doing such an out-of-language solution means sort of giving up and saying "there's no way to make a language with maintainable generic code, so let's just test most of the possible inputs to these bad functions." I am hoping that someone else can come up with something better.The text was updated successfully, but these errors were encountered: