If you’re coming from C++ especially, where the move/copy situation is ridiculously confusing (IMO), but also from a simpler “reference by default” language like Java, this has profound impact on what’s intuitive in the language.
For the C++ comparison, this is a pretty good article: https://radekvit.medium.com/move-semantics-in-c-and-rust-the...
This makes completely logical why some things must be Box<>'ed and borrowed. Why you cannot treat Trait or even impl Trait like any other type. Why sometimes it's ok to have impl Trait as your return type while in other cases it's impossible and you must Box<> it.
Third important realization is that things borrowed out of containers might be moved 'behind the scenes' by the container while you hold the borrow, so you are not allowed to mutate container while you are holding borrows to any of its contents. So it's ok, to instead hold indexes or copies or clones of keys if you need.
Another observation is that any struct that contains a borrow is a borrow itself. Despite using completely different syntax for how it's declared, created, it behaves exactly like a borrow and is just as restrictive.
Last thing are lifetimes, which don't have consistent (let alone intuitive) syntax of what should outlive what so are kinda hard to wrap your head around so you should probably start with deep understanding what should outlive what in your code and then look up how to express it in Rust syntax.
Rust is syntactically very similar to other languages, but semantically is fundamentally different beast. And while familar syntax is very good for adoption I'd also prefer tutorials that while showing the syntax explain why it means something completely different than what you think.
I found it frustrating enough that I put the language down and just went back to using C++.
In Rust it's also obvious because every = is a move. Confusion comes from tutorials pretending for too long that it's not.
> whereas in Rust, similar behavior seemed to depend on the object type.
It's best to think of that as an exception to the rule created specifically for numbers and other similar small, cheaply copied things.
If you need to move `a` but `a` has a trait Copy then you copy instead.
> Rust has its own move operator as well, despite destructive move being the default behavior for assignment.
I don't think that's true? Rust has a `move` keyword but it's a part of closure definition that makes it take all the variables from its environment by move, even if it doesn't need it. Unless you are talking about something else...
At a high level, I think the most unintuitive part of moving in C++ compared to Rust is that it can silently degrade into a copy without anything indicating it. In Rust, trying to use a value after it's been moved will give you a compiler error, at which point you can reconsider whether you do in fact want to explicitly copy or if you made a mistake or need to refactor. In C++, the only way I'm aware of to verify whether a value is actually moved or not is to use a debugger. The benefit for requiring explicit copies is similar to having bindings be immutable by default and requiring an explicit `mut` annotation; if you start out enforcing the constraint that things should be moved or immutable, fixing it later if you find out it won't work that way only requires adding `.clone()` or `mut` in one place. On the other hand, if you start out with implicit copies or mutability by default and then want to change it later, it can be a lot more work to refactor the places where the variable is used to not violate this, and it may not even be possible in some cases.
You can ignore that in other languages, but this comes at a cost later.
Joke btw. That thread is a hilarious trainwreck - surely the final nail in the coffin for the Rust advocates who so often deny anything about Rust is difficult to learn.
I don't mean that as an anti-Rust jibe, in fact I'm planning to get back to it this year (having given up in despair last). I like much about it, and think it's tremendously practical for many purposes. But it just is a difficult language, no question.
Easy is relative. I suspect a major reason I found it easy was because I didn't try to solve lifetime problems, I just cloned things. I also had primarily been using C++ in school so I was pretty familiar with pointers and, to some extent, ownership. Plus my initial foray into CS was driven by a desire to do exploit development professionally, so lower level details weren't scary at all.
Any blog about learning Rust for beginners should just contain information that helps the reader decide _whether_ she should put in the time required for learning it, then refer to the great Rust Programming Language book that's really hard to surpass.
The reference is great as well, though personally I miss a part that formally defines the type system in its current form (there are some papers about lifetimes, but they are very hard to read).
(They seemingly don't use more apt comparisons with OCaml and Haskell, for instance, not expecting the reader to know them.)
So yeah there's quite a lot that people wouldn't know.
> Closures (lambdas). Rust supports closures (also called Lambdas, arrow functions or anonymous functions in other languages).
That's misguiding.
Closures are not lambdas. Lambdas are just syntax, but the whole point about closures is that they capture the enclosing environment (have access to variables where it's defined). Rust's documentation states just that. Closures may or may not be lambdas.
In above example of "Inner functions" (which is also a closure) that would be more clearly explained if the inner function used an outside variable. Not all languages can do that.
I really hope to start using Rust in 2023, probably for some kind of API gateway experimentation
So true. It’s never too late, you’re never too old, there’s never something else you need to learn first, just do it.
1. "Operators" have different meanings. `1 | 2` and `1..=2` mean something different in patterns than in expressions. Here is a silly example: https://rust.godbolt.org/z/76Ynrs71G
2. Ambiguity around when bindings are introduced. Notice how changing a `const` to a `let` breaks the function: https://rust.godbolt.org/z/aKchMjTYW
3. Can't use constant expressions, only literals. Here's an example of something I expect should work, but does not: https://rust.godbolt.org/z/7GKE73djP
I wish the pattern syntax did not overlap with expression syntax.
This actually bit me in the a__ when I misspelled my enum variant and instead match created a variable named like that, that captured everything and I got only a warning and very wrong code.
I think there should be `let`s inside match block if matching creates some variables.
fn func<'a>() means that 'a must outlive execution time of func
but
T:'a means that references contained in T must live longer than 'a
and
'a:'b means 'a must live longer than 'b (that's consistent at least)
Maybe:
fn 'a:func() {
or fn func() 'a:{
would be better for indicating that 'a should outlive function execution.Maybe some directional character would be better than : (> is probably out of question because of <> generics)
----
I feel like structs that don't have 'static lifetimes because they contain some borrows should have it indicated in their name.
For example:
struct Handle&<'a> { n:&'a Node }
or even struct Handle&'a { n:&'a Node }
or struct Handle& 'a:{ n:&'a Node }
to indicate that 'a must outlive the struct.Then you could use it:
let h = Handle& { n: &some_node };
Maybe functions that create non-static struct might have & at the ends in their names.Like
vec![].into_iter().map()
but vec![].iter&().map()
You could easily see that you are dealing with something you should treat like a borrow because it contains borrows. Such structs would be sort of named, complex borrow and raw '&' borrow would be anonymous or naked borrow.Not sure if it would also be nice to differentiate structs with &mut
----
I would just like to separate lifetimes syntax from generics syntax because those two things have nothing to do with each other from the point of view of the user of the language.
----
I would also like to have
while cond {} else {}
where else is only entered if cond was false from the start. But that's a wish not specific to Rust.
That you can write very few interesting programs without venturing into the heap with Box, Rc and such and into internal mutability with Cell and RefCell.
Then it quickly raises to the power of other languages and surpasses them with "pay for only what you use" mentality.