rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

RFC: Attributes in function return type position

Open not-my-profile opened this issue 4 years ago • 17 comments

#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

not-my-profile avatar Nov 25 '21 01:11 not-my-profile

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.

~~Presumably if you wish to place a return type attribute on a unit-returning function, you must explicitly state the return type? ie.~~

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.

Diggsey avatar Nov 25 '21 01:11 Diggsey

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 Vec<Page> is returned as json than to just say that the example HTTP endpoint returns json. Similarly, I'm not sure it'd be better to have

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 must_use, but it seemed a useful study.)

So I think overall my first instinct here is "weak no due to insufficient motivation". But that's weakly held, so could change.

scottmcm avatar Nov 25 '21 03:11 scottmcm

Small nit: could you define DSL in the RFC text please? Since I'm not quite sure what you mean there.

clarfonthey avatar Nov 25 '21 03:11 clarfonthey

@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).

not-my-profile avatar Nov 25 '21 06:11 not-my-profile

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.

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

Boboseb avatar Nov 25 '21 12:11 Boboseb

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 type1 and type5 annotates the return type (int*), and function_type annotates the function type (in this case int* (*) (int *)), while [[function]] annotates the whole function.

ogoffart avatar Nov 25 '21 21:11 ogoffart

https://github.com/rust-lang/rfcs/pull/3201#issuecomment-979473450 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:

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.

C++ has the [[nodiscard]] attribute, which for the purposes of this discussion is basically #[must_use]. There's also [[noreturn]], but Rust already chose a never type for that use case. I believe that's it for standardized attributes that apply to a function's return type, as opposed to the entire function.

Ixrec avatar Dec 17 '21 17:12 Ixrec

I think this would be useful for data transformation. For example, picture a web framework handler like so

#[handler]
async fn get_user() -> #[json] User {
    User{...}
}

petar-dambovaliev avatar Feb 06 '22 07:02 petar-dambovaliev

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 #[non_null] being added to *mut to indicate that it is not null (probably not a good idea in Rust, but there is a lot of precedent for this in languages like Java, so let's run with it). Then you could have fn foo(x: #[non_null] *mut T) or fn foo() -> #[non_null] *mut T. But now it's kind of ambiguous whether that non_null is attached to the return type or the return value.

This may be a distinction without a difference, I'm not sure. #[must_use] is an interesting example. We have "must use" types, and arguably it makes sense to think of -> #[must_use] String as returning a value of type #[must_use] String.

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?

nikomatsakis avatar Feb 07 '22 15:02 nikomatsakis

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 T, and then refactor the function to return Result<T>. Should the annotation change? Is it possible to change it to do the right thing in all cases?

nrc avatar Feb 08 '22 09:02 nrc

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.

clarfonthey avatar Feb 08 '22 17:02 clarfonthey

@clarfonthey

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.

nikomatsakis avatar Feb 09 '22 15:02 nikomatsakis

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 -> is a punctuation mark. It'd be like asking if " and , are nouns or verbs.

ssokolow avatar Feb 09 '22 19:02 ssokolow

I'm a bit skeptical on the practicality of annotating types specifically, since flowing them through generics properly seems very awkward. Trying to get Vec<(A, #[non_exhaustive] B)> to work scares me, vs Vec<(A, NewType<B>)> just generally does.

scottmcm avatar Feb 09 '22 20:02 scottmcm

A precedent for this is WGSL, they heavily use return type attributes. This would be useful for example in rust-gpu, they currently use MaybeUninit out params instead.

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)
}

kanashimia avatar Apr 17 '24 21:04 kanashimia

if we use the alternative syntax from https://github.com/rust-lang/rfcs/pull/3201#issuecomment-1032849255 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)
}

kennytm avatar Apr 18 '24 02:04 kennytm

What happened if we placed #[cfg] on the return type?

fn foo() #[cfg(unix)] -> u32 {
    todo!();
}

if we follow the rule of #![feature(stmt_expr_attributes)] (rust-lang/rust#15701) this should emit an E0658 "removing the return type is not supported in this position" error.

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.

kennytm avatar Apr 18 '24 03:04 kennytm