My first impression about the Rust language

Hi everyone,

I've started to learn Rust, this language has huge potential and it isn't hard to learn, I like Rust... but I think Rust should be more strict (one way to do things) and handle lifetimes implicitely. So these are some of my impressions.

  1. Semicolon and return keyword

Golang, Python, Ruby, Swift, Groovy don't have semicolon, it isn't mandatory with JavaScript, I'm used to using the semicolon since I'm developping in C++ but is it a useful symbol to make compilation faster today? To return a value in Rust you can just use a value without the semicolon, BUT the return keyword is mandatory since you can't do "early returns" without this keyword:

fn f() -> i32 {
    if true {
        // early return
        return 123;
    }
        
    return 456; // alternative pattern, just use "456"
}

=> I think that the semicolon isn't useful, and that Rust creators should have kept the return pattern since it will remain a mandatory keyword for "early returns"

  1. More than one keyword to create a loop?
loop, while, for

=> Golang can do that with one keyword

  1. Two syntaxes for references?

The following code is valid:

struct A {}
let mut mut_a: A = A{};

fn borrower1(a: &mut A) {
    // Code
}

fn borrower2(ref mut a: A) {
    // Code
}

// Equivalent
let b = &mut_a;
let ref c = mut_a;

=> Why & and ref ?

  1. Any difference between a struct tuple and an alias tuple?
struct TupleFromStruct(f32, f32);
type TupleFromAlias = (f32, f32);
  1. Explicit lifetimes should be implicit

In Rust each parameter have different lifetimes, sometimes it's necessary to put explicit lifetimes (when you have more than one parameter), do you think someday the Rust compiler will handle the lifetime implicitely? I think it can :slight_smile:

This would make the language less orthogonal. Why? Because almost all syntactic constructs are expressions, this includes assignment, all three loop variants, match expressions, and more. So by forcing you to exit a function with return, would make the function body different from an ordinary block!

For example, this is valid and sets x to 0

let x = {
    let y = 10;
    0
};

I quite like the syntactic distinction between loop types, it makes it easy to see when something funky is happening (if loop is used), if there is some odd stopping condition (usually with while loops), if the code is boring (with for loops, boring can be a good thing), or if the code is being fancy (iterator methods, like for_each, any, etc.).

This is mostly historical, but there is a good reason for ref and &. You can use ref in nested patterns and do some fancy things with lifetimes that can't be done with just & and the current auto-project references through patterns. And & is far more ergonomic than having to create a new binding every time you wanted a reference.

Yes,

let _: TupleFromStruct = (0.0, 0.0); // won't compile
let _: TupleFromAlias  = TupleFromStruct(0.0, 0.0); // won't compile
let _: (f32, f32) = TupleFromStruct(0.0, 0.0); // won't compile

let _: TupleFromAlias = (0.0, 0.0); // will compile
let _: TupleFromStruct = TupleFromStruct(0.0, 0.0); // will compile

It can't, because explicit lifetimes are about intent, and the compile can't divine your intent, :slight_smile:.

6 Likes

If I understand this point correctly, the possibility of allowing the semicolon on the return value (without the return keyword) has been suggested before by team members, but I don't know if it's going anywhere. Removing ; everywhere is, of course, a backwards compatibility non-starter.

borrower1 takes a mutable reference to an A. borrower2 takes an A by value and binds a to a mutable reference. Playground example. The motivation is discussed in the reference; you'll mainly see it in match blocks and the like.

Another difference is that you can implement different traits per type, different methods, associated consts and functions, control privacy, etc. Things you can't necessarily do with a type alias, especially a standard one.

Some can be elided today and perhaps more cases will be covered in the future. But it will never cover all cases.

Returning 'c would have been valid in the code below based on flow analysis. Or 'b under a more conservative analysis. But changing from 'c to 'b or changing from 'b to 'a would be a breaking change.

// imagine this became dynamic or random
const WANING_MOON: bool = true;
fn foo<'a, 'b: 'a, 'c: 'b>(_a: &'a String, b: &'b String, c: &'c String) -> &'a String {
    if WANING_MOON {
        c
    } else {
        b
    }
    // Imagine in some future version I want to return _a
}

You can't use a type alias to prevent type errors if the types being aliased are the same. The compiler won't complain if you use mi where you intended to use km. It sees them both as f32. The newtype pattern, what you called a struct tuple, will.

playground

The semicolon is needed for disambiguation and in order for the language to remain whitespace-insensitive. In a language all about correctness, ambiguity and reliance on invisible characters is just not acceptable.

Those two syntaxes designate different language constructs: expressions and patterns, which are dual to each other.

Those are not always possible to infer unambiguously, so sometimes you have to annotate them. (Even when they can be inferred, they might not do exactly what you want, so there must be a way to override.)


In general, please consider learning more about Rust before criticizing it mainly out of mere misunderstandings of its nature and design goals.

2 Likes

Just learning the language myself so I don't have much to address on the main topic, but in regards to the early return, you can have early returns in a function without using the "return" keyword so far as I've seen. something like:

let var1:i32 = if true {33} else {55};
let var2:i32 = if !true {33} else {55};
println!("var1: {}   var2: {}", var1, var2);

or with functions:

fn test1() -> i32 {
    if true {
        33
    } else {
        55
    }
}

1 Like

if/else is an expression, so that is in a sense the same as:

fn test2() -> i32 {
    33 + 55
}

(It's not really "early" as nothing follows the if/else.)

1 Like

hmm, are you sure? I don't follow where you're saying an if statement that returns a integer value, followed by an else statement that returns an integer value, will result in the two integer values being added

Point is not in the addition - of course, semantics of these two pieces of code is different. The idea is that there's no "if statement, followed by else statement" - there's "if/else expression".

1 Like

ah, ok yes using an if/else statement to represent a function was misleading, I just wanted to show the similarity in use

hmm, are you sure? I don't follow where you're saying an if statement
that returns a integer value, followed by an else statement that
returns an integer value, will result in the two integer values being
added

He isn’t saying that. He is saying ‘if/else’ is an expression.

1 Like

Indeed, I made the mistake of trying to illustrate that "lining the inside of a function" with an 'if/else' expression will take care of any conditional early return situations, or 'match' statement etc. I would like to learn more of the differences between functions, macros, and lambda's though, eventually