-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Proposal: Type variables #4821
Comments
It's unclear to me how the proposal affects this. Wouldn't |
This is similar to what Odin does; there, you only specify the add :: proc(a: $T, b: T) -> T {
return a + b;
} To this end, Zig's could be: fn add(a: $T, b: T) T {
return a + b;
} As a sidenote, Odin also has extra specialization syntax for this where you can do stuff like this: sum :: proc(elems: $T/[]$E) -> E { // T=[]E, E=elem type
tot := E(0);
for e in elems {
tot += e;
}
return tot;
} .. meaning that |
You can also declare mutliple "polymorphic" types in the signature if you'd like: add :: proc(a: $T, b: $U) -> T {
return a + T(b);
} |
Vow, I love this proposal. It resolves #4820, and gives an elegant way to support generic params. |
It's not that I don't like this proposal, but I fail to see how it solves your example problem.
Isn't this exactly the same, except for giving it a name, as
?? Another question I have is about this example:
What do you expect to happen if I agree the explicit type parameters can get messy, but it also makes it a bit easier to reason about what types things are. I'm fearful of moving in the same direction as C++ templates, where you get weird errors about types which you have no idea where they come from or what they actually are. |
@Qix- You used this example, but what is fn add(a: $T, b: $T) $T {
return a + b;
}
// comptime_int / u8 / ... ?
add(1, 2) //-> 3 Especially when the return type does not depend on the parameters, you will have to specify it explicitly again: _ = add(@as(f32, 1), 2); That being said, I actually like the current way of doing fn myfunction(comptime T: type, a: T, b: T) T {} it makes it very obvious what is going on when calling a "generic" function. |
This works great when one type parameter is "shared". It doesn't work so well in contexts like this though: // exaggerated example for the sake of demonstration:
fn myFunction(comptime T1: type, comptime T2: type, comptime T3: type, comptime T4: type, a: T1, b: T2, c: T3) T4 {}
// less messy, but less information
fn myFunction(a: var, b: var, c : var) DeduceReturnType(a,b,c) {}
// or if it was possible to "attach" type predicates to the var keyword
// type predicate: fn(type) bool
fn myFunction(a : var(isType1), b: var(isType2), c: var(isType3)) DeduceReturnType(a,b,c) {}
|
At some point you also have to wonder what you're doing that you need so many generic type parameters which are all (potentially) different. I've been wondering, what are valid use cases for My question is: Should |
@user00e00 Good point. I never had a function with more than one type parameter, though. So I can't really comment on that. |
@BarabasGitHub Because explicit types require knowing the type up-front, which means either duplicating types for explicit calls, or using fn add(comptime T: type, l: T, r: T) T {
return l + r;
}
fn mul(comptime T: type, l: T, r: T) T {
return l * r;
}
fn muladd(l: var, r: @TypeOf(l), a: @TypeOf(l)) @TypeOf(l) {
// Extreme example, of course.
return add(@TypeOf(l), mul(@TypeOf(l), l, r), a);
} This is, of course, an extreme example - it's meant more to demonstrate how complicated things can get, especially when mixing the two paradigms together. However, with type variables, none of this is an issue - it's reduced to what is probably the minimum amount possible (aside from having type-less parameters entirely) while still enforcing proper types at the same time. // Proposed syntax
fn add(l: $T, r: $T) $T {
return l + r;
}
fn mul(l: $T, r: $T) $T {
return l * r;
}
fn muladd(l: $T, r: $T, a: $T) $T {
return add(mul(l, r), a);
}
|
I personally dislike the proposal. I myself prefer highly verbose types but even despite my own bias I think that there are some pretty big issues here. In particular take the example you gave here:
I'd imagine for most programmers, even without knowing much about Zig or anything about the compiler, you can fully reason about how the return types got deduced and therefore how implicit casting rules would behave. For
Here its not immediately obvious how the compiler would deduce what Now its not actually possible to do so, as the order of the arguments matter, and if you tried doing That's the beauty of the verbose syntax though, you don't need to understand much at all to know why calling Maybe I'm just being salty here, but this whole |
The example uses In which case, only one of those
You could just make the signature |
I'm not a huge fan of the single Further, there wouldn't be a single variable that determined the type; instead, Zig would ensure that all declarations with the type variable had equal types. I feel pretty strongly that a single |
That's what AFAICT, it's the same as now with |
@Tetralux Yes, it can be done that way, but I feel pretty strongly that it creates more confusion than it solves by allowing |
Yes, I'm arguing that
I agree this is horrible and I'd argue that you should do it like so:
Not so bad, is it? And then you have questions like what happens if I call: |
I find it interesting that you feel this way, considering that Odin has very similar approach to what's being suggested here, yet has significantly better error messages, and I've never had problems figuring out what was wrong from them, so I suspect your objection is unfounded. |
@Tetralux I have no experience with Odin, so it's possible that it can be made to work. Personally I prefer being explicit and keeping things simple. Sometimes it can be a bit more work to type, but it is often easier to understand when reading. One function in Zig std where I think an explicit parameter might have been better is Half the time you end up writing this:
while you might as well do
Of course that also makes tests like these longer:
which now needs to be:
but I don't feel that's all that bad. |
There are 3 different APIs you can make for functions that take multiple parameters of the same generic type, and they each have different semantics. Here they are using existing Zig features: // Explicit type parameter
fn add(comptime T: type, a: T, b: T) T {
return a + b;
}
// First parameter determines the type
fn add(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
return a + b;
}
// Peer type resolution
inline fn add(a: anytype, b: anytype) @TypeOf(a, b) {
// This intermediate function only exists to facilitate peer type resolution between a and b,
// then implicitly coerce each value to the type.
return add_impl(@TypeOf(a, b), a, b);
}
fn add_impl(comptime T: type, a: T, b: T) T {
return a + b;
} There were a few ideas in the original post of this discussion and in follow up comments that would give syntactic sugar for the non-explicit versions above: // First parameter determines the type
fn add(a: $T, b: T) T {
return a + b;
}
// Peer type resolution
fn add(a: $T, b: $T) $T {
return a + b;
} In order for syntactic sugar to be added to the language, it needs to encourage programmers to more clearly convey intent, write more correct code (avoid bugs), or generally align with paradigms idiomatic to Zig. In addition, the use case needs to be common enough to justify the increased complexity of the Zig language. While this proposal does encourage more clearly conveying intent and quite possibly avoiding bugs, there is one major problem. This proposal is just the tip of the iceberg of pattern matching features that should probably follow. Here are more pattern matching features I would expect if I can declare a parameter of type
We can see examples of metaprogramming languages that have attempted to accomplish these use cases such as C++'s templates, Rust's type bounds, even Java's bounded type parameters. Each metaprogramming language has a different set of features (see Rust's If we were to add a robust pattern matching metaprogramming system to Zig, it would be a huge amount of additional complexity to the point where I would consider it an additional language within Zig. But everything that that additional language would do is already possible to write today using regular imperative Zig code that runs at compile time. You can even import third-party metaprogramming libraries that implement arbitrarily sophisticated pattern matching. Every one of the numbered use cases above can be implemented by a comptime function that takes a type and emits a compile error if it doesn't conform to some constraints. The only drawbacks to using a userspace solution for pattern matching are that the compile errors are a little more confusing, and that the function signature does not encode any rules using the Zig type system; you have to rely on the function's documentation to know how to call it. There's a bit of a fork in the road for the future of Zig. We can either attempt to implement a sophisticated metaprogramming language like most other modern languages have done, or we can just have |
Zig seems to be set up to support generic functions quite well, but is lacking some of the flexibility or expressiveness I'd personally hope for. Currently, you can use
param: var
(or, soon to be,param: anytype
if #4820 lands) and then@TypeOf
to infer the types.However, this is a bit messy not only in readability, but maintainability; if the type of
a
changes, butb
and/or the return type should not, then all three types will have to be updated.There exists the other way of doing it, with first-class
comptime
type variables:But this requires explicit type specification at the call-sites, which also includes a bit of maintenance overhead.
What would be helpful is some syntactic sugar: Type identifiers prefixed with an immediate
$
(e.g.$FooType
but not$ FooType
) are type variables that can be used in place of types anywhere within the function (including the signature) and the compiler will automatically deduce them.It may even be syntactic sugar for normal type variables (in the second form above) if that's easier to implement (it'd also be easier to reason about, too).
For example, the above signature becomes:
This last form should be nearly semantically similar to the first form (depending on how the type checking would be implemented) and identical to the second form (if this proposal were to be implemented as syntactic sugar for the type variables).
The text was updated successfully, but these errors were encountered: