-
-
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
Proposal: Allow TypeOf to be used immediately after a name of a parameter is declared #6615
Comments
This is a pretty interesting idea. It took me a few minutes to see what you were getting at. So this would mean that when the compiler is evaluating each parameter type, if the symbol for that parameter appears in that type expression, then it resolves to the value passed in for that parameter by the caller. So if I have fn foo(x: @TypeOf(x)) void { }
foo("hello");
foo(123); On each of the calls above, This also adds an new semantic feature, the ability to normalize parameter types. For example, fn NormalizedString(comptime T: type) type {
// if a pointer to an array, return the slice type
...
}
fn foo(x: NormalizedString(@TypeOf(x))) void { } Wow this is a really interesting idea! |
Exactly, that would make possible type constraints aka concepts, arbitrary type manipulation and more. For example a function that widens the type of a generic parameter: fn WidenIntOrFloat(comptime T: type) type {
if (T == u8 or T == u16 or T == u32 or T == u64) {
return u64;
} else if (T == i8 or T == i16 or T == i32 or T == i64) {
return i64;
} else if (T == f32 or T == f64) {
return f64;
}
@compileError("unsupported type");
}
fn example(x: WidenIntOrFloat(@TypeOf(x))) void {}
// const a: u8 = 5; example(a); calls example(u64)
// const b: u32 = 55; example(b); also calls example(u64)
// const f: f32 = 1.5; example(f); calls example(f64) |
@Rocknest I disagree the second idea, it make parameter becomes to a undecidable state(both for human and compiler) Consider following code fn Evil(comptime T: type) type {
return struct { // or anything that will create new type
const I = (if (@hasDecl(T, "I")) T.I else 0) + 1;
};
}
fn problem(arg: Evil(@TypeOf(arg))) void {
@compileLog(@TypeOf(arg).I); // how to solve the equation
}
comptime {
problem(.{});
} Although it is theoretically possible to report errors early by limiting the evaluation depth. Update: use .{} insteads of struct { const I = 0; } |
I think this is a great idea that solves #1669 in an intuitive way. |
@codehz I'm not sure where the issue is with your snippet; comptime calls are cached if that's what you're worried about? |
@codehz Where is the problem exactly? Given your code: fn Evil(comptime T: type) type { // line 1
return struct { const I = T.I + 1; }; // 2
}
fn problem(arg: Evil(@TypeOf(arg))) void { // 4
@compileLog(@TypeOf(arg).I); // 5
}
problem(struct { const I = 0; }{}); // 8 Lets see how it is executed at comptime step by step:
As you can see control flow will not even reach compileLog statement. A compile error would be emited:
|
Simulated in status quo Zig: https://godbolt.org/z/6KKP47 fn Evil(comptime T: type) type {
return struct { const I = T.I + 1; };
}
fn problem(_arg: anytype, arg: Evil(@TypeOf(_arg))) void {
@compileLog(@TypeOf(arg).I);
}
comptime {
const a = struct { const I = 0; }{};
problem(a, a);
} |
fn Evil(comptime T: type) type {
return struct { const I = (if (@hasDecl(T, "I")) T.I else 0) + 1; };
}
fn problem(_arg: anytype, arg1: Evil(@TypeOf(_arg)), arg2: Evil(@TypeOf(arg1)), arg3: Evil(@TypeOf(arg2))) void {
@compileLog(@TypeOf(arg1).I); // 1
@compileLog(@TypeOf(arg2).I); // 2
@compileLog(@TypeOf(arg3).I); // 3
}
comptime {
problem(.{}, .{}, .{}, .{});
} https://godbolt.org/z/P7a5af |
I find this syntax to be a bit confusing. While the concept is legitimate, it's an edge case that requires a leap in understanding compared to other uses of I'm not sure getting rid of a keyword is reward enough for the price of "hiding" an otherwise very friendly and convenient feature. |
I think solving the issues this solves is great, but this syntax is pretty hard to understand and explain. |
@kristoff-it it is not a goal to remove the keyword, the idea is to give more power to userland so features like type constraints, type manipulation etc can be implemented. I think @tadeokondrak it does not have to be |
@Rocknest for type constraints we can already implement everything (simply by putting the typechecking at the beginning of the function), what we would get from #1669 (or this proposal) is basically only convenience, I think. What do you mean with "type manipulation"? |
@kristoff-it yes we can do that in status quo Zig, but it is not a part of a function prototype, and most of the time users do not bother to add checks and if something not used properly you get a compile error somewhere deep in the code. Its a convenience that would encourage to write more expressive, type-safe and fail-fast code. For a basic example of "type manipulation" see #6615 (comment) |
Nice idea but I agree with @kristoff-it - this feature just adds a bit more convenience by introducing a new way of doing the same thing. I think that type checking at the beginning of the function is perfectly fine. Actually I find below easier to understand.
|
I don't think so. I really like the idea, as it allows reducing code bloat. Consider the With this proposal, the explosion of types can be reduced a lot: const AnyConstSlice(comptime T: type) type {
return []const std.meta.Child(T);
}
fn car(slice: AnyConstSlice(@TypeOf(slice))) std.meta.Child(@TypeOf(slice)) {
return slice[0];
}
fn cdr(slice: AnyConstSlice(@TypeOf(slice))) @TypeOf(slice) {
return slice[1..];
}
test "explosion"
{
var first_0 = car("1");
var first_1 = car("12");
var first_2 = car("123");
var first_3 = car("1234");
} This would only instantiate one
But it's obvious in this example also that fn car(slice: AnyConstSlice(@Inferred(slice))) std.meta.Child(@TypeOf(slice)) {
return slice[0];
} or even more restrictive: fn car(slice: AnyConstSlice(@Inferred())) std.meta.Child(@TypeOf(slice)) {
return slice[0];
} To me, it looks like this proposal is better than #1669, as it solves the same problem, but also allows code optimizations as the ones above. |
Good point. One more concern with readability.
Combining Condition1 and Condition2 into one function is not always wanted because we can get many functions to cover all the cases. |
Preach brother, too many code paths in the stdlib take the yolo approach and crash and burn if the constraints are not satisfied. From the syntax pov I prefer the |
The recursive TypeOf is actually create a equation, and the compiler needs to solve the equation before it can instantiate the correct type. so I think Comparison
|
I don't think the proposed application of fn proposed(x: Foo(@TypeOf(x)) void { }
fn current(_x: anytype) void {
const x: Foo(@TypeOf(x)) = _x;
} And is thus not even a semantic change, but only a syntactical transformation (in theory) which can further be used to reduce the number of function instantiations |
@MasterQ32 but the |
That's what this proposal proposed. And i also find the double-naming bad (as you can read above) and proposed that we should do some other builtin for this kind of type-inference |
@codehz there is no recursion or any possibility of circular dependency since parameters are evaluated from left to right, even if you can reference self, you cannot reference the next parameter, so Also i don't see a problem in this example |
There is also another way to transform this code into status quo Zig: fn call(x: Func(@TypeOf(x)), y: @TypeOf(x)) void {}
call("abc123", "xyz"); As you can see there is only one scope, no new compiler magic required, and const P = struct { val: anytype };
fn p(val: anytype) P {
return .{ .val = val };
}
fn Func(comptime T: type) type {
return []const u8;
}
comptime {
var param_x = p("abc123");
@compileLog(@TypeOf(param_x.val)); // | *const [6:0]u8
param_x = p(@as(Func(@TypeOf(param_x.val)), param_x.val));
@compileLog(@TypeOf(param_x.val)); // | []const u8
var param_y = p("xyz");
@compileLog(@TypeOf(param_y.val)); // | *const [3:0]u8
param_y = p(@as(@TypeOf(param_x.val), param_y.val));
@compileLog(@TypeOf(param_y.val)); // | []const u8
} By the way without indirection through function |
@Rocknest looking at your last example: fn foo(x: Func(@TypeOf(x)), y: @TypeOf(x)) void {} I think this example demonstrates the "weirdness" of This gives us reason to use different syntax and/or builtin to refer to the "caller's version of x" and the "function's version of x". fn foo(x: Func(@CallType()), y: @TypeOf(x)) void {} Note that this builtin doesn't take any arguments. If we can think of use cases, we may want to access another parameters call type, or multiple call types in a single expression. So we could have it take an argument as well, like this: fn foo(x: @CallType(x), y: CheckAndTransform(@CallType(x), @CallType(y))) void { } EDIT: since we can use capitalization to indicate it is a type, maybe fn foo(x: @Call(x), y: CheckAndTransform(@Call(x), @Call(y))) void { } |
@marler8997 well it depends how you look at it, if we get rid of 'parameter type' scope and transform function prototype evaluation into a comptime block you will see that weird behaviour is preserved, and |
@marler8997 lets consider how your example would be evaluated Edit: Its probably wrong how i framed this proposal, the question here is what happens when a parameter is referenced inside the scope of its type evaluation? Another solution could be keep existing Edit2: also struct needs to be solved: |
@Rocknest, |
Yeah I had similar thoughts. You could also reference the parameter by name using a string: fn foo(x: @Call("x")) Depending on how builtin calls are analyzed, this might make the implementation more simple, otherwise there might have to be custom argument evaluation for this particular Another alternative to allow us to use the symbols without special casing for the arguments in this case would be to implement this at the syntax level, i.e. fn foo(x: calltype(x)) In any case, there's a million ways to skin this cat. I think among everyone's comments we've enumerated alot of them. Hopefully we'll land on one people can mostly agree with since I think the feature at it's core would be quite useful. |
Discussed this issue with @andrewrk and @marler8997 . This proposal grants two new abilities:
This proposal has clear ergonomic benefits for writing generic code. However, everything that this proposal introduces is already possible by writing two functions - one which accepts anytype and performs coercion and validation, and another that receives the modified parameters. So this is purely an ease of use feature. There are a lot of problems with generic code. Generic code is harder to read, reason about, and optimize than code using concrete types. Even if it compiles successfully for one type, you may see bugs only later when a user passes a different type. Generic code with type validation code has an even worse problem - the validation code has to match with the implementation when it changes, and there’s no way to validate that. So the position of Zig is that code using concrete types should be the primary focus and use case of the language, and we shouldn’t introduce extra complexity to make generics easier unless it provides new tools to solve these problems. Since everything in this proposal is possible with a wrapper function, we don’t think this is worth the complexity it adds. Especially since this feature is only for generic functions, which should be used sparingly. Because of that, we've decided to reject this proposal, with the aim of keeping the language simple. We know this may be somewhat unexpected, given the popularity of this issue. However, having a simple language means that there will always be places where it would improve ergonomics to have a little bit more language. In order to keep the language small, we will have to reject many proposals which introduce sugar for existing features. |
Currently a function parameter is only visible to the
@TypeOf
after next parameter's scope starts.I propose to allow to use
@TypeOf
after only a name of a parameter is declared. Can it be useful? Yes, that would allow to put type constraints in a function signature instead of a function body. This basically #1669 without the new syntax.What happens when i provide a type which is different from the type that
@TypeOf
returned? Compiler will check if the value of the argument is coerceable to the new type, if it is not then its a compile error.Optional Part
If this proposal is implemented then these two expression will be equivalent:
This means we could remove keyword 'anytype'.
x: @TypeOf(x)
looks a bit verbose to me, see #1669 (comment) for an alternative builtin@Infer()
, in short instead of overloading@TypeOf
create a new builtin that implements proposed functionalityx: @Infer()
,x: Constraint(@Infer(), isWriter)
.The text was updated successfully, but these errors were encountered: