But
const THING: OnceLock = default();
would, since OnceLock: Default
.
(Written assuming some form of use std::default::Default::default;
exists.)
But
const THING: OnceLock = default();
would, since OnceLock: Default
.
(Written assuming some form of use std::default::Default::default;
exists.)
Come to think of it, actually, if we get Pre-pre-RFC: syntactic sugar for `Default::default()` - #75 by ekuber along with some form of the inferred struct names on initializer feature, it could also potentially be
const THING: OnceLock = .{ .. };
without needing impl const Default
.
We can also argue that there's no reason that the const
type can't be inferred from the initializer, specially if not pub
...
const THING = OnceLock { .. };
even worse: const and static items have to have an explicit type even if they're part of a function body.
That's discussed above.
That's why const { … }
is getting stabilized in under a week.
Kotlin supports this style. It is called Single-expression functions. So you can either write:
fun double(x: Int): Int = x * 2
or you can define it as a regular function:
fun double(x: Int): Int {
return 2 * x
}
I used Kotlin as my main language for years. I found this option to be more annoying than helpful. It reduced my productivity because I constantly had to chose which style to use. So a form of choice paralysis.
Not only do you feel less productive because you had to constantly make a choice, you also had to deal with annoyingly big git diffs when changing styles.
IMHO it would be better to relief users of the burden to chose style by only allowing the current style.
fn double(x: i32) -> i32 {
x * 2
}
In Kotlin you had to use the return
keyword in regular functions, which made single-expression functions quite a bit nicer in some cases since you could omit return
. But in Rust the use of return
is already optional, so it would be less of an improvement to add support for single-expression functions to Rust.
for everything in your post. I have exactly the same experience in C# -- it's nice there for saving the
return
, but even with that I'm not convinced the choice is worth it for functions.
IMO the real benefit is not omitting the return
, but having the function take up only one or two lines instead of three.
indeed, but i think single expression if
statements are a much more effective way of reducing the number of trivial lines in a program. i know i used to use if (...) return;
a lot in C
Maybe there could be a more general syntax for single-expression blocks anywhere, involving some initial punctuation or keyword instead of brackets; like:
=>
operator, borrowing from single-expression match arms:
if ... => return;
fn() -> i32 => 10;
loop => game_step();
"do
" keyword:
if ... do return;
fn() -> i32 do 10;
loop do game_step();
Edit: For ambiguity reasons I don't think you should be able to use chained syntax like if _ => _ else
, but I think punctuation solves the other most common issue with unbracketed expressions. It makes it obvious that you can't just insert another statement:
if condition
=> println!("this line runs if condition is true");
println!("this line always runs");
Plus if you comment out the first println
, you don't accidentally start executing the second println
. You'd get a compile error complaining about a lack of curly brackets or arrow operator.
I tend to disagree on this too, I actually found if
expressions to be pretty inconvenient most of the time. The biggest problem is rustfmt
hate for single line if
expressions, as the cutoff for the line length is really small (and yes you can customize that, but you have to do that for each project where it's acceptable). Splitting and if
expression over multiple lines means it takes up at least 5 lines (!) instead of just 1. For comparison, a separate if ... { return ...; }
takes up 4 lines even in Rust and also avoids indentation drifting for the else
case. This becomes especially bad when you want to include that into another expression. And sure, you can make a separate binding where it could be short enough, but then you no longer have just an expression and you have to introduce a block, which adds to the number of lines.
In practice I actually found myself preferring to use match
on bool
s or match ()
followed by _
patterns with if
guards just because they use less lines and keep the code more succinct.
And since you brought C into the comparison, C has the ternary operator which is pretty good for short expressions. The case where it falls short is when in Rust you would use an if
/match
expression with a relatively big body, but this discussion was about short code so this is a bit out of scope. I'm not saying that I prefer C, I actually think Rust being expression oriented is definitely better and safer, but writing short and succinct code can often be surprisingly hard.
Another case where this is painful is with small temporary closures. Often the type inference is not strong enough to infer their signature, forcing you to make it (at least partially) explicit. This in turn makes the closure longer, and can often result in it being splitted over multiple lines. This is however not really a matter of syntax but rather of better type inference.
That would just make the "which do I use?" questions even worse. I don't want to pick between
if b {
foo();
bar();
} else {
qux();
}
and
if b {
foo();
bar();
} else => qux();
(Nor have a discussion about which rustfmt
should use.)
That's fair, it definitely forces people to develop a rule of thumb to avoid choice paralysis. I'm not really sure that it's worth having both.
To bikeshed a little bit, I think =>
is a much better symbol than =
for single-expression functions.
=>
inherits intuition from match arms, which evaluate expressions with "input" from a pattern.
=
inherits intuition from assignment, associating an identifier/pattern with a fixed value.
For instance, I think it fits with something like the assignment of a function to its name:
fn new = (a: u64, b: String) -> Self;
but I think it's a bit awkward for assigning expressions to entire function signatures.
=>
is a thicker separator, helping to clarify that the body and signature are distinct parts.
It's similar to how the lazy logic operators are thicker than those with a higher precedence; more intuitive to read even if you don't know the precedence (ind > 0 && ind - 1 < len
).
On the other hand, the smaller "=
" appears to group the body with last part of the signature, which isn't really the right intuition. The expression is being "assigned" to the entire signature, not just the return type or the where
clause.
It has a nice symmetry with the ->
symbol.
It communicates that the type and the expression are related without necessarily making one subordinate to the other.
It's easier to read if broken up across multiple lines:
fn add(x: i32, y: i32, z: i32)
-> i32
=> x + y + z
If types are inferred, it's potentially easier to refactor (one letter to transform ->
into =>
).
To continue on the bikeshedding:
In my intuition =
(analogous to its use for assignments) would be more appropriate for function aliases, e.g.
fn new(…) -> Self = Self::default; // reuse the body of `default` for `new`
In contrast
fn new(…) -> Self = Self::default();
reads as "The function new
is the result of Self::default()
" instead of the intended "The function new
evaluates to the result of Self::default()
" to me.
Is this feature really worth it at all? It does increase the complexity of learning the language, for what? To save a couple of curly brackets and a Self
? Is this even a real problem we are trying to solve? Especially since Rust-analyser can autocomplete much or this anyway.
I think most would agree that constructor functions don't need special syntax sugar, but some existing ideas just coincidentally resolve the issue raised by the post (A, B).
At the same time there are good reasons to have some kind of bracket elision in general. For example, imagine you have a large code block like:
match letter {
Letter::A => println!("A"),
Letter::B => println!("B"),
Letter::C => println!("C"),
// ..
Letter::X => println!("X"),
Letter::Y => println!("Y"),
Letter::Z => println!("Z"),
}
and you want to apply a minor control-flow effect to that entire block. You'd have to find both ends of the block, wrap it in brackets, and preferably indent the contents.
It would be much more efficient if you could just prefix the block:
if can_print => match letter {
Letter::A => println!("A"),
Letter::B => println!("B"),
Letter::C => println!("C"),
// ..
Letter::X => println!("X"),
Letter::Y => println!("Y"),
Letter::Z => println!("Z"),
}
After testing you might realize that this change doesn't work out, or it was just temporary from the start. Either way, reverting the change is just as easy (whereas reverting bracketing and indentation is tedious even with a decent editor in my experience). It's like the difference between function and method calls; there's a reason people use infix notation.
Now, it's arguable whether this conflicts with Rust's goals as a language (maybe it makes code harder to review), but bracket elision is definitely useful for prototyping volatile ideas. I know that I often wanna be able to prototype lots of changes (minor or major) without investing too much effort into each one, while also keeping the code easy to navigate.
I think that is very unreadable, so I'm very much against introducing the general version of this. It also seems prone to bugs similar to Apple's goto fail bug where it isn't clear how much the if stement applies to.
Thanks for that example, it made me dislike this feature suggestion even more. And I think I'm also echoing the reasons @scottmcm dislikes for the general (not just constructor).
indeed, and for this exact reason i never use single-expression if when if writing C unless it can fit on a single line alongside the conditional.
this is basically the maximally bad version of what i try to avoid: stretching a single expression across possibly dozens of lines.
I don't think it's an issue since things like this can be picked up by rustfmt or clippy. (btw, I think that it is readable and would argue that it should be allowed by clippy but that's a different argument)
I run into this occasionally and agree it is a pretty clunky experience. I disagree with extending the language for this purpose, but I could also see ways that rust-analyzer or even rustfmt could be extended to make this case better. For example, rustfmt could accept the arrow syntax, but always format it to the correct brace syntax.