Thanks, I hate it. Is there any time where you want to use this? Like if Rust worked like other languages and just figured out if it was an expression or statement are there times where you would need an override?
I'm playing around with this in the compiler explorer and I'm now even more confused why you want this.
let x = {
println!("Don't mind me in the struct definition");
3
}
assert_eq!(x, 3);
It's not so much that this ability is specifically put in for some reason. It's just something that falls out of several other things.
Rust chose a "curly braces and semicolons" syntax because that's the sort of syntax that is normal in the sorts of PL spaces Rust wants to be used in. I am not sure exactly why being expression-oriented was chosen, but if I had to guess, it would be due to that just generally being considered a better option among many people at the time it was chosen.
So okay, you want expressions, and you want semicolons. Therefore, "you separate expressions with semicolons" is a pretty natural outcome. And since we're expression oriented, "a block is an expression that evaluates to the final value" is near tautological. And since it's an expression, it can go anywhere an expression can go.
Not being able to do this would mean creating specific restrictions against it, and then having to memorize when things don't follow the usual rules. That's more complicated than just letting expressions be expressions.
> I'm playing around with this in the compiler explorer and I'm now even more confused why you want this.
It works with RAII to create a temporally isolated resource scope, think context managers / using / try-with-resource; it provides a scratch space where you limit collisions with the rest of the function and avoid the risk of mis-reuse of those values; and before non-lexical lifetimes it was critical to limit borrow durations. It's also routinely used in combination with closures, to prepare their capture. Similar to C++ capture clauses, but without special syntax.
It's essentially a micro-namespace inside the function.
This kind of thing is useful for memory management. Anything you allocate within the scope that isn't returned gets dropped at the end.
You can use this to e.g. acquire a Mutex guard, move/clone something out of the mutex, and ensure it gets dropped as quickly as possible.
let x = {
let items = vec![1, 2, 3];
items[1] // copied out of the vec since usize implements the Copy trait
// compiler inserts drop(items) here
};
// items is no longer valid
assert_eq!(2, x);
It also comes up in if/else blocks, which have exactly the same syntax and semantics (i.e. they are expressions, not statements):
let condition = true;
let x = if condition {
println!("condition is true");
5
} else {
println!("condition is false");
10
};
assert_eq!(5, x);
edit: and of course, function blocks work exactly the same way! It's neat.
let dx = {
let prev_x = x;
x = get_x();
x - prev_x
};
often, it's slightly better cpp style scoping blocks if nothing else? there are tons of other little QoL things it enables though, but they're all going to be little ergonomics things that only seem worth it if you've used the language for awhile