I really should learn some Rust 😂
Rust
Welcome to the Rust community! This is a place to discuss about the Rust programming language.
Wormhole
Credits
- The icon is a modified version of the official rust logo (changing the colors to a gradient and black background)
Don’t 99% of people don’t need the efficiency boost of not having a garbage collector. Instead learn a Functional Programming Language like Scala, OCaml or F#
It's not only the efficiency boost. What about safe concurrency? Or about the tooling - does sbt really compare favorably to cargo? (I'd say "no", having used both over multiple years)
Most web applications don’t even need concurrency, what they need is a language that’s easy to read with as little cruft as possible, rust is not that.
I’m saying the best web language would be one that is as simple as python or js with the type security of an ml language. Rescript comes to mind as well.
Or both? Functional languages are good to be familiar with, but so is Rust's strict ownership model. Both lead to better code in all languages.
One mistake they did unfortunately ship though is bind patterns that look like variable names.
There was a recent langdev Stack Exchange question about this very topic. It's a bit trickier to design than it might seem at first.
Suppose we require a keyword -- say var
-- before all binding patterns. This results in having to write things like
for (&(var x1, var y1, var z1), &(var x2, var y2, var z2)) in points.iter().tuple_windows() {}
,
which is quite a bit more verbose than the current
for (&(x1, y1, z1), &(x2, y2, z2)) in points.iter().tuple_windows() {}
.
Not to mention you'll have to write let var x = 0;
just to declare a variable, unless you redesign the language to allow you to just write var x = 0
(and if you do that, you'll also have to somehow support a coherent way to express if let Some(x) = arr.pop() {}
and let Some(x) = arr.pop() else {todo!()}
).
Suppose we require a keyword -- say const
-- before all value-matching patterns that look like variables. Then, what's currently
match (left.next(), right.next()) {
(Some(l), Some(r)) => {}
(Some(l), None) => {}
(None, Some(r)) => {}
(None, None) => {}
}
turns into either the inconsistently ugly
match (left.next(), right.next()) {
(Some(l), Some(r)) => {}
(Some(l), const None) => {}
(const None, Some(r)) => {}
(const None, const None) => {}
}
or the even more verbose
match (left.next(), right.next()) {
(const Some(l), const Some(r)) => {}
(const Some(l), const None) => {}
(const None, const Some(r)) => {}
(const None, const None) => {}
}
and you always run the risk of forgetting a const
and accidentally binding a new match-all variable named None
-- the main footgun that syntactically distinguishing binding and value-matching patterns was meant to avoid in the first place.
Suppose we require a sigil such as $
before one type of pattern. Probably the best solution in my opinion, but that's one symbol that can no longer be used for other things in a pattern context. Also, if you're already using sigils before variable names for other purposes (I've been sketching out a language where a pointer variable $x
can be auto-dereferenced by writing x
), doubling up is really unpleasant.
...So I can understand why Rust chose to give the same, most concise possible syntax for both binding and value-matching patterns. At least compiler warnings (unused, non-snake-case variables) are there to provide some protection from accidentally turning one into the other.
I went the "only let
introduces bindings" route, and I'm pretty happy so far:
if (left.next(), right.next())
... is (Some(let l), Some(let r)) { /* use l and r */ }
... is (Some(let l), None ) { /* use l */ }
... is (None, Some(let r)) { /* use r */ }
... is (None, None ) { /* use nothing */ }
}
Yeah, they could literally have the same syntax as now, but w/ let
when introducing a variable. So:
match (left.next(), right.next()) {
(Some(let l), Some(let r)) => {}
(Some(let l), None) => {}
(None, Some(let l)) => {}
(None, None) => {}
}
Or you could put the let
before the Some(...)
as let Some(l)
, which allows us to keep the current if let Some(...) = ...
syntax. Either of those would feel more consistent than the current implementation.
I completely forgot that unit structs/variants define their own associated consts. I wonder if in patterns the type can be used instead of the associated const though? That might resolve a lot of the headache. It'd mean changing the way the ident is resolved to looking in the type namespace though.
const <block>
already works as a pattern I believe? That could be used instead for constants.
Literals would always work in-place as constant expressions.
As in using consts (or variables you think are consts) as refutable patterns? Yeah this was an oversight I'm sure.
One option is an edition change requiring a const
keyword, so
match foo {
const BAR => {},
baz => {},
}
Right now they use a lint to try to warn the dev though.
In addition to that, I have my own list of things Rust should not have shipped with, but did.
Drop struct initialization syntax
...
Named parameters using =
I would like to take this a step further: support default arguments (fn func(a: int = 0); func(); func(a=0)
), and then have struct initialization work like that. It's really nice in Python, though I'd like some rules to require args w/ default values to use the assignment syntax, and _require args w/o default values to be in positional order (our codebase is littered with long_func_name(long_var_name=long_var_name, ...)
where function calls are incredibly long and redundant). That does a few things for us:
- make defaults more obvious - no more
T::new()
since now you can just doT()
if you just want defaults - make special cases obvious -
func(1, 2, some_arg=value)
makes it obvious thatvalue
is special - eliminates the
T{...}
syntax, since you can just use theT(...)
syntax
We probably disagree about requiring positional args to use the assignment syntax, but both your preference and mine can be enforced w/ conventions (i.e. give your variables relevant names to the function).
Replace
impl
and eliminate theimpl X for Y
syntax
I disagree, though I can see where you're coming from. Many OOP languages use the first option, and the second try to reuse keywords.
But you're also missing one use of the impl
keyword: fn func() -> impl Trait
.
I come from Go and I honestly like the syntax there:
func (T self) fn_name(args...) {
}
Many don't. I like it because it shows that these methods aren't part of the type T, they're "attached" to it. That's much closer to how this actually works that how Java represents it. It's a small thing, but for something low-level like Rust, I think that makes sense.
Also, it makes room for implementing additional functionality on existing types outside of that package. This doesn't work exactly as I'd prefer, but the syntax opens the door to that.
Stop using macros to emulate varargs
I'm on the fence about this. The println!("{}", val)
syntax is really nice, and that's one of the common uses for varargs. If your varargs are all the same type, you can use slices: fn func(varargs: &[T]); func(&[1, 2, 3])
and avoid macros entirely. If your types are inconsistent (i.e. C's varargs), you're going to have a rough time w/ any vararg syntax and will need a macro anyway.
I agree that macros shouldn't be abused to emulate varargs, but I don't think we actually need a vararg syntax and can use existing tools.
Drop range syntax
Agree. Use a built-in like Python's range(...)
instead of fancy syntax, and have the type ([T]
, &[T]
, or vec<T>
) be inferred at compile time. This can probably just drop-in to everywhere the range syntax is being used currently. It's a little more verbose, but it's way clearer.
Drop array and slice syntax
Are you suggesting using keywords/methods instead? So [T].slice(...)
or even Array<T>.slice(...)
? I think that's reasonable, and it can work similarly to Vec
, but it would complicate the syntax a bit for my vararg proposal. But yeah, [T, count]
is pretty ugly, but I'm not convinced Array<T, count>
is all that much better.
I think your post could benefit from a couple examples.
Make generics use [] instead of <>/::<>
I prefer D's !
approach: T!U
or T!(U)
. I would switch macros to T#(...)
instead, since #
is already used for things adjacent to macros anyway. But to minimize impact, we could just use #
for generics instead.
I'm not a fan of the []
for generics, and I disagree w/ Go using that. <>
is awkward due to conflicts with comparison operators, so I agree that it needs to go.
Fold Index and IndexMut into Fn trait family
Eh, I'm less interested in this one, but I don't have a strong opinion. Calling *T.index(...)
isn't a big ask...
Remove the hierarchy between Eq/Ord and PartialEq/PartialOrd traits
This is certainly annoying, but surely the right solution is to just make floats implement Eq/Ord, no? I can understand someone preferring the PartialEq/PartialOrd behavior, but that's way more niche than just treating 0
and -0
as equal and NaN
as always unequal.
Drop ::
Agreed.
Drop as
Disagree. I prefer changing it to only do type conversions (i.e. f32 -> f64 is valid, but f64 -> f32 isn't). Value conversions should use a different syntax.
Drop if-let
Disagree. Either keep as-is, or put let
next to the new symbol (if Some(let i) = ...
). I don't like chaining, if you want that, just introduce a block or something. It's a bit more verbose, but it's way less confusing.
Other languages have destructuring, and this is basically the same thing, and should be handled similarly. For example, in javascript: let {some: myVar} = option_object;
(i.e. object that looks like {some: value}
. An option is basically just an object w/ two members, so the syntax should be similar: let Some(my_var) = option_object else default_value;
. Throwing an if
in front should merely do something like Python's walrus operator, if my_var := some_call()
, which evaluates whether the expression is truthy, but in the Rust case, it would evaluate whether the assignment was able to be made.
I think if-let makes sense, but don't expand it.
Drop procedure syntax
Why? What value does -> ()
provide? Why not elide that?
Rectify “almost rules” into “always rules”
This sounds pretty pedantic. Most of those are quite intuitive, and a lot of this is redundant given the rest of your post.
Remove significance of semicola
Disagree. I like that semicolons have meaning in Rust, because in most languages they just feel unnecessary, and languages w/o them feel awkward since you're generally limited to one statement per line.
Thanks for your reply, some replies below!
requiring positional args to use the assignment syntax
Not sure, maybe my wording isn't clear enough. What I intended to say is that arguments can be named, not that they have to. In any case, the order of arguments must match the order of parameters, named or not.
But you’re also missing one use of the impl keyword: fn func() -> impl Trait.
That removal could actually happen, so I didn't list it. (Rust started requiring dyn
and disallowed naked trait returns with edition 2018. So dropping the impl
in that position might not be completely impossible like the other uses of impl
.)
Are you suggesting using keywords/methods [for array and slice syntax] instead?
Yes, just methods.
I can understand someone preferring the PartialEq/PartialOrd behavior
You can have both – that's what's being made possible by them not being in a hierarchy.
I think if-let makes sense, but don’t expand it.
It's a bit late for that, isn't it? ;-)
Why? What value does -> () provide? Why not elide that?
What value is provided by keeping it? Why a syntactic special-case for exactly that type and not any other random type?
languages w/o them feel awkward since you’re generally limited to one statement per line
Then fixing that might make sense. :-)
Drop if-let
Over my cold dead body. if-let-else is such a fantastic pattern. Makes everything an order of magnitude more readable. Works so nicely for unwrapping opts/errs.
Their suggested replacement is closer to C#'s is
keyword:
if foo is Some(foo) {
// ...
}
The issue here is that we still have let-else, which can't be translated as easily:
let Some(foo) = foo else {
todo!();
};
I don't think the alternative to let-else is too bad.
That's not an alternative, it's removing let-else
entirely. It's equivalent to this:
let i = if let Some(i) = opt_number {
i
} else {
return 0;
};
let-else
is specifically a feature that allows you to use a refutable pattern to deconstruct a value by providing a diverging else
branch.
Removing let-else
is the whole point of the linked article series:
Being able to do to everything Rust does, but without Rust's zoo of if-then-else
, match
, if-let
, let-else
etc.
I'm curious, have you used Rust much? Most of those changes just feel like "rust should be more familiar to me" changes.
Also:
As Rust 2.0 is not going to happen, Rust users will never get these language design fixes
Isn't necessarily true for most of your suggestions. Since most of them are just changes to syntax semantics and not language semantics they could be made in an edition.
Interesting perspective. Not sure I agree with most of the suggestions though.
Some of the earlier ones remind me of C#'s records. Were they inspired from them?
Some of the later ones just feel like Go to me.
I like the idea of dropping syntax for ranges. It does feel like the syntax just leads to confusion.
Named parameters are problematic because of parameter names becoming significant to the API. See Python's *
and /
in parameter lists (like def foo(a, *, b)
for example).
Some of the earlier ones remind me of C#'s records. Were they inspired from them?
No, that stuff is much much older.
Named parameters are problematic because of parameter names becoming significant to the API. See Python’s * and / in parameter lists (like def foo(a, *, b) for example).
I think the name problem is overblown, you can always have an annotation to facilitate name changes.
The "drop the array and slice syntax" is just nuts. With 0 justification.