Repeated `return type` could be removed from function body

struct User {
    name: String,
}

impl User {
    fn new() -> Self {
        // 1. return type explicitly mentioned 
        // 2. seems redundant, since we already this function returns `Self`
        // 3. does it look elegant ? Probably verbose ?
        Self {
            name: "Alex".to_string(),
        }
    }

    fn new2() -> Self {
        // 1. return type `Self` implicitly assumed
        // 2. compiler can apply return type check
        // 3. IDE can provide IntelliSense for autocompletion based on `-> Self`
        // 4. pleasing to eyes ?
        {
            name: "Alex".to_string(),
        }
    }
    
    fn new3() -> Self {
        // same as new2(), but explicit use of `return`
        return {
            name: "Alex".to_string(),
        };
    }
}
fn main() {
    let _user = User::new();
    let _user2 = User::new2();
    let _user3 = User::new3();
}

This syntax is ambiguous when using abbreviated field names (name: name simplified to name).

Even when not ambiguous to the parser, it's just less clear whether it's a block of code or a struct literal.

Ability to use the Self alias and lack of explicit return keyword are already concessions made to save typing.

5 Likes

As much as I do believe your ideas for improving Rust seem genuine and not stupid, I do believe they show you might want to familiarize yourself a little bit more with the language; your ideas ([1][2]) seem to focus very much on issues of surface syntax, and do little to improve the problems that more experience users of Rust might have with the language. With experience in a programming language, one’s views of what does “look elegant” also often changes.

Yes, Rust iterators can be more verbose than functional programming; yes Rust can be more explicit in types than other languages with even more type inference capability.

But whenever you notice “this seems redundant” about some syntax, you need to consider the bigger picture. How much of an issue is a little bit of extra typing really? Was there any reason why it was required in the first place? Like… does it follow more general principles that would need to change?[1] How much of a new set of general principles could you make out of any feature that do changes them, and how much complexity might this add?[2] Do slightly modified versions of the code exist where the information isn’t redundant – then perhaps it wasn’t fully redundant in the original code either?[3]


  1. If you aren’t sure why something is the way it is, a good first step could also be to ask on the Users Forum for feedback on why things are designed the way they are. If people list good reasons and explain underlying design principles you weren’t aware of, maybe it was a great learning opportunity; and if people express that “it’s tedious / it’s the way it is / wish Rust had a better way for this” it can be great motivation to spend some actual effort into finding and fleshing out concrete ideas for improvement that don’t have zero change to ever lead anywhere to begin with ↩︎

  2. E.g. how much special-case magic would you need? E.g. for StructName { field: value }-style syntax, becoming { field: value } what about the case of StructName {}? Because {} already has a clear (and different!) meaning! ↩︎

  3. For example, with your iterators ideas, note how collect can create collections of other types than you started with. ↩︎

19 Likes

This suggestion would be quite useless in practice. The more stuff you punt onto type inference, the more likely is type inference to break. At best you'll have to specify types explicitly anyway, at worst you'll have some entirely inscrutable error message. This means that in practice you need to regularly specify types anyway: either via explicit monomorphic function parameters, or via type ascription on variables, or via fully qualified method call syntax, or struct literals, or using forward inference on sufficiently explicitly typed functions.

Struct literals in particular are commonly used directly as method receivers or parameters of generic functions, which means that most cases won't benefit from extra inference. The proposal also doesn't work for enum struct variants, which are syntactially the same as structs, but conceptually quite different and not even types (so it doesn't make sense to infer enum variants). It would be confusing if a syntax sugar would be allowed only in some of syntactically similar cases.

It's also unclear how that inference should be represented in the type system. Note that inference in Rust is quite complex, and you can't just directly infer the type of a struct literal at its use site, in general. It would have to work similarly to the inference for numeric literals, which means that your proposal is really about adding an entirely new type: abstract structs which have only names and types of fields, but not the type of struct itself. This turns it from an "add a little sugar" proposal into a full-blown "modify the type system to add structural types as a first-class concept", which is niether what you intended nor something likely to pass, at least without extensive research and justification. This also makes inference more likely to break in confusing ways, like already happens with integer literals.


More importantly, conciseness on its own isn't a virtue, and repetition is important for human communication: it brings error correction and structure. A knee-jerk attempt to remove repetition isn't to be entertained, and speaks mostly of your lack of experience with the language. Languages overusing purely syntactic abstraction end up write-only.

2 Likes

You might have noticed that the compiler already detects the new2 case:

error: struct literal body without path
  --> src/main.rs:20:9
   |
20 | /         {
21 | |             name: "Alex".to_string(),
22 | |         }
   | |_________^
   |
help: you might have forgotten to add the struct literal inside the block
   |
20 ~         { SomeStruct {
21 |             name: "Alex".to_string(),
22 ~         } }
   |

In order to do so, the parser has to do some really unsavory things that we rather not have as part of the language. Diagnostics code can get away with doing things that the language can't, because the cost of "getting it wrong" is lower when the worst that can happen is a bad error, while a mistake for language implementation can have dire consequences. The implementation can go the extra mile and have unbounded lookahead, but the language itself should not require unbounded lookahead (to give out an example). Otherwise alternative implementations, be them other compilers or any other kind of tool that wants to analyze Rust code will need to have a more complex implementation just to cover those corners of the language. We've gone as far as requiring the turbofish (::<Ty>) in expressions precisely to avoid having to do things like intertwining parsing and name resolution.

11 Likes