-
-
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
Generic function types are unintuitive and unhelpful #14187
Comments
Consider this case: const std = @import("std");
const ArrayList = std.ArrayList;
fn foo(comptime T: type) ArrayList(T) { ... }
fn bar(comptime T: type) std.ArrayList(T) { ... } The functions This becomes even worse when you consider the inverse case, where same name refers to different things in different scopes: const x = struct {
const G = std.ArrayList;
fn foo(comptime T: type) G(T) { ... }
};
const y = struct {
const G = std.StringHashMap;
fn bar(comptime T: type) G(T) { ... }
};
Another complaint I have with the proposed solution is that |
Hm, yeah, I hadn't considered these issues with a naive IR comparison, so I guess that complicates things. The issue here just boils down to function aliasing - trivially aliased functions and types should be considered equivalent, but an IR comparison wouldn't do that. What we ideally want here is some kind of method to partially evaluate the ZIR so that such aliases are resolved before comparison. To that end, I need to look into the specific structure of the ZIR to determine if this is easily possible. `x.foo` and `y.bar` obviously have different types, yet they would be considered identical under a naive IR comparison. I'm not familiar with the exact structure of ZIR, but... would they? I thought the ZIR would refer to the constant by a different index here (i.e. this reference is resolved at the Either way, I can't answer these two points properly until I've had a more in-depth look at the ZIR structure, so I'll stop talking for now and respond again once I've looked more deeply at how it could work.
Why can't this be considered a declaration of |
Comparisons between generic function types should use the internal representation and not the lexical expressions, obviously. That should eliminate any issues with namespacing and trivial renamings. More problematic are comparisons involving nontrivial type aliases like BTW, xref #6997, which proposed a more limited version of this. |
Why would that solve the issue? |
That only means that type analysis needs to be performed at some appropriate later stage. But that wouldn't be any different from ordinary type checking. E.g., if you have a function like this fn square(x: i32) Int {
return x*x;
}
test "foo" {
const five: i32 = 5;
try std.testing.expect(square(five) == 25);
} you cannot tell whether it type-checks without looking at the actual definition of
That's another example of a non-trivial type alias. This particular case has a trivial comptime condition and will probably be resolved into Edit: Fix mistake in code snippet. |
Having thought about this some more, I don't believe there's a good way for my original design to work. So I'm going to retroactively adjust this issue to focus on my alternative solution. New ProposalEven aside from the fact that it's generally inconsistent (#5893), the term Therefore, the term Note: this proposal combined with #9260 would eliminate the term |
How about the keyword True, the name is not perfect for the times where the
|
EDIT: I believe my original proposal here was infeasible, so have adjusted it to a smaller (and fairly simple) change.
Updated Proposal
Tangentially related: #9260
Problem
Currently, generic functions have very strange-looking types when you introspect them.
The type being printed here is nonsensical -
anytype
isn't a valid return type! You cannot write the type of this function in standard Zig without doing something like@TypeOf(foo)
. And besides,foo
can't return "any type" - it's limited to the type of its input. This is especially weird when you realise that it makes function types which are definitely different technically identical:Because both of these functions are generic, they're considered as having the same type, despite definitely being different.
This gets even weirder when we look at functions where parameter types depend on other parameters:
This one is extra misleading, because the type printed is entirely syntactically valid, but it's not the same thing as
bar
's real type in practical terms. That means you can pass functions in contexts where they don't actually make sense, like this:The summary here is that Zig treats any generic type as
anytype
in a function type, even if that type is much more constrained. This has several negative consequences:fn (comptime T: type) T
isn't considered a valid type outside of a function definition, andanytype
can't be written as the return type. You have to resort to dirty hacks like@TypeOf
on a dummy function.anytype, anytype
case above.Proposed Solution
Make
fn (comptime T: type) T
et al real types.The compiler clearly already tracks this information somewhere (otherwise, the last example above would have much more disastrous consequences). Exposing it as a part of the type makes the type system both more intuitive and more practically useful. Function types would coerce to any valid full or partial instantiation; e.g.
fn (anytype, anytype) void
coerces fine tofn (u32, anytype)
, orfn (anytype, u32)
, orfn (u32, u32)
.This wouldn't necessarily require function types to have specific names associated with them. Instead, this could be represented internally with some kind of index or pointer, and the required parameters could just be given generic names (
x
,y
, etc) if the type is printed via@compileLog
.Comparisons between generic function types do have one strange property: they require equality comparison between expressions. For instance,
fn (comptime T: type) std.ArrayList(T)
isn't the same thing asfn (comptime T: type) std.StringHashMap(T)
, and we can only detect this equality by actually comparing the return type expressions. This is admittedly a bit strange, but it doesn't raise any issues I can immediately think of - we don't need to do any evaluation (it seems fine to me forfn (comptime T: type) T
andfn (comptime T: type) id(T)
to be considered distinct). I assume this would basically consist of checking equality between some blocks of ZIR, which doesn't sound too complex, but I could be missing something.The main sticking point in my mind in terms of language usage is how
@typeInfo
and@Type
(related: #10710) operate under this system. The latter is a non-issue; construction of generic function types atcomptime
is already disallowed, and I don't really see a reason to change this (it's a niche within a niche, and probably indicates overly complex comptime logic). In terms of@typeInfo
, any solution I can think of to expose the info feels overcomplicated and not very practically useful (sophisticated in-language checking of generic function types is, like, a niche within a niche within a niche), so I don't really think we need to do that - the current structure (where we just hide the types of generic parameters and return types) seems okay to me. I did think about having something like anInstantiate: fn (args: []const *const anyopaque) type
for generic function types, but that's equivalent to a@TypeOf(func(params))
, so seems pretty pointless.(One question: why is
std.builtin.Type.Fn.Param.is_generic
a thing whenarg_type
is already optional? Is there not a one-to-one correspondence there?)Other Options
In my eyes, the main practical issue with the status quo is the inability to write functions with generic return types. We could just make
anytype
a valid return type when writing function types (although this feels weird if #9260 gets in (:crossed_fingers:), as that proposal otherwise eliminatesanytype
from the language). This is certainly a simpler solution implementation-wise, but it's still quite unintuitive for users.We could further improve that by instead introducing a new keyword - maybe
generic
- that indicates an unknown generic type which isn'tanytype
. That would make the signatures much more intuitive to read, and slightly more specific - for instance, the type offn id(x: anytype) @TypeOf(x)
becomesfn (anytype) generic
.The text was updated successfully, but these errors were encountered: