-
-
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: @Result
to match cast builtins inference API
#16313
Comments
@ResultType
to match builtin inference API@ResultType
to match cast builtins inference API
If you're returning One big drawback of this is that you're adding an invisible comptime parameter which silently adds more instantiations of your functions. |
I really like having custom casting functions - f.e. enhanced by In general the interface of The cast builtins currently already unwrap error unions and optionals: IMO hiding this step from the user (and discarding the additional information) would be a bit of a shame though, but I can't quite figure out how to make it work.
Maybe we would want both options, accessible as
That is true; in my custom Although for explicitness here's another idea: Instead of implicitly passing a fn(comptime R: type = @ResultType(), x: R) R {return x;} Downsides:
The most in-line with current syntax would actually be a capture IMO: // `keyword |capture|` has precedence in Zig
fn |R| f(x: R) R {return x;}
// could use an additional keyword
fn resulttype |R| f(x: R) R {return x;}
// (could use an operator, but no precedence of this in Zig, therefore I like it less)
fn -> |R| f(x: R) R {return x;}
// could also put it after the function name, so `fn <name>` remains Ctrl+F-/grep-/searchable
fn f |R| (x: R) R {return x;} |
I like this idea a lot What if it was |
related, |
@Jarred-Sumner So you're thinking something like this instead? fn parseInt(buf: []const u8, base: u8) std.fmt.ParseIntError!infer T {
...
} The That could also be complementary if the infer syntax was already available to be used on parameters (as in the mentioned #9260). fn foo(bar: infer T) infer K {
...
} |
I like this proposal as is, but I can also see a slight modification:
Advantages:
Disadvantages:
|
related: #447 |
I think the |
I like |
Hmm, personally both feel pretty similar. What about |
To me I agree |
As it was already mentioned in other discussions it's a bit cumbersome to use std.log.info("{}", .{@as(u32, try std.fmt.parseInt(content, 10))}) In the same time Zig already has a perfect way to specify the type of a constant/variable/function-argument with a colon syntax: const x: u32 = try std.fmt.parseInt(content, 10); It would be really nice to allow the same syntax to specify the type of an arbitrary expression (like in Julia lang, but they use double colon for this purpose). So the example above will look like this: std.log.info("{}", .{try std.fmt.parseInt(content, 10): !u32}) Another example from here with buildins. Old syntax: return @intCast(@as(i64, @bitCast(val))); New syntax: return @intCast(@bitCast(val): i64); |
I don't have a strong opinion either way on this proposal, but I have a few notes:
|
Yes, the If we have
|
I have a mostly neutral stance on this proposal. Just a though on the name, let's have the following code: pub fn build_something() @ResultType() {
var something: @ResultType() = undefined;
switch (@typeInfo(@ResultType()) {
....
}
return something;
}
It doesn't really work with pub fn build_something() anytype {
var something: anytype = undefined;
switch (@typeInfo(anytype) {
....
}
return something;
}
It should support assignment: pub fn build_something() @ResultType() {
var T = @ResultType(); // comptime
var something: T = undefined;
switch (@typeInfo(T) {
....
}
return something;
}
From here, we could argue that the following would also make sense: (@N00byEdge suggestion) pub fn build_something() anytype {
var T = @ReturnType(); // comptime
var something: T = undefined;
switch (@typeInfo(T) {
....
}
return something;
}
Now the argument I could have for it, is easier code maintenance if we have something like: // myfield has type IsoDate
mystruct.myfield = deserialize(IsoDate, str);
// If we change myfield type, we need to find the deserialize call and change the type too
// with proposal
mystruct.myfield = deserialize(str);
// No need to change the type here
For types that could be automatically casted, this could prevent some bugs and ensure the returned type is always exactly the type of the destination variable that the returned value is assigned to (similar to why first argument was removed from builtins). Taking this into account, I could be slightly in favor of this. |
@kuon Basically agree with everything you've got here. I too am neutral on this.
But we have to be careful, that is an argument against it too, as mentioned before:
|
I don't think this is actually a very strong argument. Comptime arguments are already passed the same as normal parameters, meaning a function's call-site can't actually always tell us when passing a different value would incur an extra instantiation. For example: foo(1, 2);
foo(3, 4); Assuming one or both of the parameters of This feature would have the same drawback as all of existing status quo and the accepted proposal (having to read the function signature to know whether it will generate separate instantiations). |
I agree with @InKryption that there are many places that function instance can be created transparently. I think this feature would be similar to functions accepting On embedded platform, when I want to limit the use of some functions to reduce binary size, I work around this problem by adding comptime assertion to the type. For example, if I want a pub fn something(myint: anytype) void {
switch(@Type(myint)) {
u8, u16 => {},
else => @compileError("type not supported"),
}
}
I actually have helpers functions for that, but you get the idea. |
Full support for this proposal! It would be so much more ergonomic and readable to be able to use this in userspace, such as with |
I'd like to propose |
@ResultType
to match cast builtins inference API@Result
to match cast builtins inference API
What will happen if the call site does not have a specific type, e.g. due to peer type resolution or anytype? |
the same you'd get with the cast builtins: |
There seem to be a lot of suggestions here on how to signal to the compiler to infer the output type of a function. Perhaps NOT specifying any output type should just mean "infer it"? It's hard to beat 0 characters as far as syntax minimalism goes. fn build_something() {
var something = ..;
return something;
} |
Yeah, just inferring is the cleanest imo. |
I agree, that it's a clean option, but now you've broke one of Zig's core principle of being very readable, even by people not familiar with Zig, now If a random user want to know the possible return type of the function, he/she will have to look for every places where that function is called to try to find a type, whereas an explicit @Result() or @ReturnType() is explicit enough that anyone, can understand that this functions return type is inferred depending on the call site. Maybe I'm stupid but If I try to imagine myself going into a code-base and finding function prototype that don't return anything I'd be pretty confused about what they are doing, especially If I see a return keyword. |
Wouldn't not specifying a type with |
Exactly, |
There's an important distinction to make between the proposed fn add(a: u32, b: u32) @Result() {
return a + b;
}
const c = add(2, 2); // error: call to 'add' must have a known result type (the error here is by analogy with existing builtins which rely on their result type: #16313 (comment)) If defining a function without any return type had this behavior, it would be very confusing for new users who might expect the return type to be inferred as Additionally, regardless of how the return type is written for such functions (omitted, |
My hope is that the following would work. fn add(a: u32, b: u32) {
return a + b;
}
const c = add(2, 2); In this case, the compiler knows that In some ways, making the above piece of code work is orthogonal to (and perhaps easier than) being able to access the inferred output type in the function body itself with |
This proposal is not really about inference of function return types by their body contents. If you have suggestions around that, that might belong in a different proposal. The goal of this proposal is to enable callsite-inferred generics. I think it's important to keep the |
What if we require inferred result locations to have an identifier, and access the inferred type in the function body via fn pythagorean(a: anytype, b: anytype) c: anytype {
const cc = a*a + b*b;
switch(@TypeInfo(@TypeOf(c))) {
.ComptimeInt => {
if (comptime isPerfectSquare(cc)) return perfect_sqrt(cc));
@compileError("result location cannot accommodate this return type");
},
.Float, .ComptimeFloat => {
return @sqrt(cc);
},
else => @compileError("result location cannot accommodate this return type");
}
} This also adds an additional benefit of enabling explicit manipulations/writes to the result location. |
@expikr I really like that idea - it basically provides a solution for this issue as well as for related issue #2765 (turns out something similar was brought up by whatisaphone there). |
@rohlem in the case of return types, perhaps this visual incongruence (not really syntactical ambiguity since operator associativity establishes the strict order) would be slightly minimized by only allowing |
@expikr I personally think we should allow it for all types. |
I have to say I'm a big fan of what this proposal would do for allocation. If we could change the pub fn create(self: Allocator) Error!anytype {
const T = @Result();
// then same as before
}
pub fn alloc(self: Allocator, n: usize) Error!anytype {
const T = std.meta.Child(@Result());
// then same as before
} then all the user needs to write is const foo: *Foo = try alloc.create();
const bar: []Foo = try alloc.alloc(10); which feels very clean. |
So I had this thought: const foo: u8 = @truncate(_, value);
// or
const foo: u8 = @truncate(infer, value);
// or
const foo: u8 = label: @truncate(infer :label, value); Not sure about the specific syntax and semantics but you get the idea. Could be done with explicit label or implicit one, although with implicit label it isn't that obvious, implicit label inference is similar to underscore arguments for lambdas in some other languages Then builtins could just take const foo: *Foo = try alloc.create(_); Additionally this is extendable to literals, would help inferring array literals in some tricky cases as an example. Downside is that callsites will be longer, especially depending on the syntax. |
The previous syntax seems preferable to all the above: const foo = @truncate(u8, value); |
Currently, the usage of const flag = b.option(bool, "flag", "An example flag"); It takes a type With this proposal, its usage could be changed to: const flag: ?bool = b.option("flag", "An example flag"); Now it's This is a simple case; I imagine there are other situations where deducing the result type from comptime arguments is more difficult, and which would benefit from this proposal as well. |
After having thought about this for a long while, I think |
I propose the following syntax. The fn parseInt(buf: []const u8, base: u8) @ResultType(T) {
// checks if `T` is an Integer type
// (`comptime_int` is safe since to even result to it means the function is called at comptime)
switch (@typeInfo(T)) {
.ErrorUnion => |U| switch (U.Child) {
.Int, .ComptimeInt => {},
else => @compileError("..."),
},
else => @compileError("..."),
};
// ...
return parsed;
} It could be used as fn parseInt(buf: []const u8, base: u8) std.fmt.ParseIntError!@ResultType(T) {
switch (@typeInfo(T)) {
.Int, .ComptimeInt => {},
else => @compileError("..."),
};
// ...
return parsed;
}
It will make less common fn foo(a: T, b: T, c: ConstructNewType(T)) @ResultType(T) {} Rather than fn foo(a: anytype, b: anytype, c: ConstructNewType(@TypeOf(a, b))) @TypeOf(a, b) {} For
I know we are saving special characters, avoid adding new unecessary constructs... And I'm all into that but since this |
Oh right, didn't consider the error union case. I'm not a big fan of "pattern matching" in general, this would basically be the first zig feature that behaved like this, but I'm also not sure how error unions could ever work without it behaving like that. |
I personally think the syntax is already there, it's just that Zig needs to recognize what you're doing. A function whose return type is passed in as an argument should be able to have that type defined by the value being assigned to, similar to how a member function of a // definition
fn foo(comptime T: type, bar: u32) !T { ... }
// i32 passed in as type parameter
var result: i32 = try foo(5); |
so if I want to ignore the return type and return something completely different I do fn foo(comptime _: type, ...) U {
// ...
} ? |
A function whose "return type is passed in as an argument" isn't just any function with a type parameter. Specifically, it's a function where the type specified in the parameter is the type that gets returned. In your example, the ignored parameter is not the type of the return, so it will stay as an explicit argument. |
Why not just make the parentheses around the fn pythagorean(a: anytype, b: anytype) (c: anytype) { ... @TypeOf(c) ... } |
Problem: Builtin-exclusive inference API
With the recent merging of #16163, cast builtins now use an API that can't be replicated in regular userspace.
For example, take the
std.fmt.parseInt
function:which currently has to be used like:
compared to the usage of a cast builtin which can be used like
For the sake of consistency, it seems like
parseInt
should be able to be used in a similar fashion.Proposal:
@Result
The introduction of a builtin
@Result
could allow the declaration ofstd.fmt.parseInt
to look like this:Benefits
Drawbacks
When should a user define a function with
@Result
inference vs. having acomptime T: type
parameter?The text was updated successfully, but these errors were encountered: