-
Notifications
You must be signed in to change notification settings - Fork 1.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
RFC: Attributes in function return type position #3201
base: master
Are you sure you want to change the base?
Conversation
d963465
to
0d9b6d4
Compare
0d9b6d4
to
db61b4a
Compare
Is there any precedent in other languages for allowing attributes on return types? It looks out of place to me, but it might just be because I'm not used to seeing attributes in that position.
I see this is covered now. fn example() -> #[attr] () {
} Also, #2565 allows attributes on closure arguments as well as functions. This RFC should also state whether closure return types can be annotated. |
So attributes on parameters were clearly helpful since there are multiple -- a function-level attribute would have had to match up the name or something, which would be awkward. But the distinction between a function and its return type are much less clear-cut to me. For example, I'm not sure it's clearer to say that the the fn to_lowercase(&self) -> #[must_use] String; than the current #[must_use]
fn to_lowercase(&self) -> String; even if you could argue that it's the return value that needs to be used. (And I acknowledge that the RFC doesn't propose changing So I think overall my first instinct here is "weak no due to insufficient motivation". But that's weakly held, so could change. |
Small nit: could you define DSL in the RFC text please? Since I'm not quite sure what you mean there. |
@Diggsey Yes as I wrote in the RFC unit-returns would need to be made explicit. I am not aware of a precedent for return type attributes in other languages, which is also why I put that very question under "unresolved questions". Thanks, good catch with the closures! I think for consistency attributes should be supported for their return types as well (I updated the RFC accordingly). @clarfonthey Thanks, I clarified that DSL stands for domain-specific language. @scottmcm Thanks, you raise a good points. I agree that the motivation for return type attributes is weaker than for parameter attributes but I think for certain DSLs they would still be desirable enough to justify their addition to the language. I agree that the json return type wasn't the best example ... I updated the motivation with a better example: #[wasm_bindgen]
impl RustLayoutEngine {
pub fn layout(
&self,
#[type = "MapNode[]"] nodes: Vec<JsValue>,
#[type = "MapEdge[]"] edges: Vec<JsValue>
) -> #[type = "MapNode[]"] Vec<JsValue> {
..
}
} is in my opinion clearly preferable to #[wasm_bindgen]
impl RustLayoutEngine {
#[return_type = "MapNode[]"]
pub fn layout(
&self,
#[type = "MapNode[]"] nodes: Vec<JsValue>,
#[type = "MapEdge[]"] edges: Vec<JsValue>
) -> Vec<JsValue> {
..
}
} So I think return type attributes would primarily be useful for specifying another return type that the function return type is somehow mapped to via the generated code. In that case having both the actual and the "mapped" return types next to each other makes the code more readable and facilitates maintenance (if one is updated the other type likely should be updated as well, which is easier to do when they're next to each other). |
C# allows attribute on return values https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/attributes/#attribute-targets but I don't remember any example |
If you're looking for other example of confusing language that can put attributes everywhere, we can take C++ [[function]]
auto [[type1]]
my_function([[arg]] int [[type2]] * [[type3]] my_arg) [[function_type]]
-> int [[type4]] * [[type5]] {
return my_arg;
} Notice that it is hard to see what exactly is annotated. Unlike in rust, the attribute sometimes comes before, sometimes after what it describes. I'm not even quite sure about what i annotated, but i think |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this idea - it's a small quality of life improvement and there doesn't seem to be much downside (it doesn't really increase the complexity of the language).
One thought is that if in the future we support throws
/yeets
or yield
as a way to specify types which are sort-of returned, we'd presumably want to allow attributes there. I don't see that being difficult or interesting, but perhaps worth mentioning?
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
TODO |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to check the parser's grammar to ensure that adding an attribute before the return type does not break anything (I don't expect that it will).
#3201 (comment) pointed out where C++ allows attributes in its syntax, but for this RFC, concrete examples of attributes that do something in return position are probably more relevant as prior art, so:
C++ has the |
I think this would be useful for data transformation.
|
I'm in favor of the spirit of this proposal (I like attributes and extensibility!) but I am concerned about a sort of "ambiguity" -- is this attribute attached to the function's return or the type that it returns. It's a subtle difference and maybe it doesn't matter, but it seems a bit ambiguous to me. I'm thinking: I could imagine us adding attributes to types in the future. As an example, consider This may be a distinction without a difference, I'm not sure. But in that case, maybe we just want to allow attributes to be attached to types instead and be done with it? Can anyone come up with examples where these two interpretations would be in conflict? |
The obvious case is where the function returns something in the implementation which is not the declared type, e.g., the future vs the value in an async function (or similar with 'yeet' syntax. etc). I wonder if there is an extension to that, like what if you have an annotation on the return type of a function, say |
If there were to be a difference between annotating the "return" part and the type itself, I would say that specifically annotating the return should go before the arrow, and annotating the type should go after. |
This makes sense to me. |
My concern would be how common the two use-cases are, since it feels strange and unnatural to me to annotate something I perceive to be in the same class of syntax as a curly brace or equals sign: fn unannotated_function() -> UnannotatedBar #[annotation_on_curly_brace] {
// Un-annotated body
#[annotation_on_curly_brace]
} let unannotated_name: UnannotatedType #[annotation_on_equals] = UnannotatedType::new(); ...possibly as unnatural as something like this: let foo =(argument_to_call_equals_as_a_function) Bar::new(); To use a natural language comparison, annotations are placed on words, while |
I'm a bit skeptical on the practicality of annotating types specifically, since flowing them through generics properly seems very awkward. Trying to get |
A precedent for this is WGSL, they heavily use return type attributes. Here is an example in WGSL: @vertex
fn main_vs(
@builtin(vertex_index) vert_id: u32
) -> @builtin(position) vec4<f32> {
let x = f32(i32(vert_id) - 1);
let y = f32(i32(vert_id & 1u) * 2 - 1);
return vec4<f32>(x, y, 0.0, 1.0);
}
@fragment
fn main_fs() -> @location(0) vec4<f32> {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
} Here is how it looks in rust-gpu currently: #[spirv(vertex)]
pub fn main_vs(
#[spirv(vertex_index)] vert_id: i32,
#[spirv(position)] out_pos: &mut Vec4,
) {
let x = (vert_id - 1) as f32;
let y = ((vert_id & 1) * 2 - 1) as f32;
*out_pos = vec4(x, y, 0.0, 1.0);
}
#[spirv(fragment)]
pub fn main_fs(output: &mut Vec4) {
*output = vec4(1.0, 0.0, 0.0, 1.0);
} Here is how it could look: #[spirv(vertex)]
pub fn main_vs(
#[spirv(vertex_index)] vert_id: i32,
) -> #[spirv(position)] Vec4 {
let x = (vert_id - 1) as f32;
let y = ((vert_id & 1) * 2 - 1) as f32;
vec4(x, y, 0.0, 1.0)
}
#[spirv(fragment)]
pub fn main_fs() -> Vec4 {
vec4(1.0, 0.0, 0.0, 1.0)
} |
if we use the alternative syntax from #3201 (comment) which does not suffer from the "don't know whether it's annotating the type or the function return" issue, the example above will become #[spirv(vertex)]
pub fn main_vs(
#[spirv(vertex_index)] vert_id: i32,
) #[spirv(position)] -> Vec4 {
let x = (vert_id - 1) as f32;
let y = ((vert_id & 1) * 2 - 1) as f32;
vec4(x, y, 0.0, 1.0)
} |
What happened if we placed fn foo() #[cfg(unix)] -> u32 {
todo!();
} if we follow the rule of And what if this is a proc-macro? fn foo() #[my_crate::my_attribute] -> u32 {
todo!();
} I suppose this would emit the error "expected non-macro attribute, found attribute macro" similar to the argument position. |
#2565 introduced attributes for function parameters. This RFC proposes the logical next step: allowing attributes for function return types. This RFC is less radical than #2602 which proposes that attributes should be allowed to be attached nearly everywhere (lifetimes, types, bounds, and constraints), which has been argued to go a bit too far resulting in too much cognitive load. This RFC hopes to increase the expressiveness of DSLs without posing too much cognitive load.
Rendered