An important “unblocker” for me when learning Rust after decades of other languages was internalizing that assignment is destructive move by default. Like many Rust intros, this sort of glides past that in the “ownership” section, but I felt like it should be a big red headline.
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.
Also very important realization is that things that are moved around (assigned to variable, moved into or returned from a function, kept as a part of the tuple or a field of a struct) must have fixed and known size. And the variable itself is not a handle. It's a fixed sized area in the memory that you named and moved something into it.
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 the copy/move situation in Rust to be far less intuitive than in C++. In C++, move semantics are obvious because they rely on std::move and the && operator, whereas in Rust, similar behavior seemed to depend on the object type. Even more confusingly, Rust has its own move operator as well, despite destructive move being the default behavior for assignment.
I found it frustrating enough that I put the language down and just went back to using C++.
Nice blog post. Nonetheless, to a new learner like me, the hardest part of rust is not its syntax; it is the ownership management. Sometimes I easily know how to implement a task efficiently in other languages but I have to fight the compiler in rust. I either need to reorganize data structures, which takes effort, or to make a compromise by cloning objects, which affects performance. Note that I do agree with rust designers that it is better to make data dependencies explicit but learning to deal with them properly is nontrivial for me.
This is exactly why I prefer Rust, eh, for everything. It forces you to think harder about your data structures and to better organize/understand your program and how data flows, gets consumed and get output.
You can ignore that in other languages, but this comes at a cost later.
The short answer is... yeah, just clone the objects. Whatever other languages are doing is going to have the same tradeoff - performance (or safety, if the other languages aren't memory safe). Iff it becomes a problem, come back later and remove the '.clone()'.
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.
In my opinion this is the way _not_ to learn Rust. These syntaxes are not important at all, and doesn't introduce lifetimes (which is by far the most important part of the language for deciding whether to use it or not).
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).
It'd be nicer if there was some way of selection which language is shown on the left side. Expecting readers to understand both C++ and Kotlin and Java and Javascript will be a stretch for most.
All of those languages adopt the C-like syntax and semantics, it shouldn't be hard for someone with familiarity with languages in that family to deduce what's being conveyed in code in languages they might not have experience with.
I thought there would be an option to select just one, but seems they are indeed just random smatterings of rust vs { Typescript, Javascript, Kotlin, Java, C, and C++ }
AFAICT, the expectation is that the reader knows at least one modern programming language from the list, and maybe is acquainted in passing with a couple of others. So at least some comparisons should click.
(They seemingly don't use more apt comparisons with OCaml and Haskell, for instance, not expecting the reader to know them.)
These features aren’t each supported by all those languages though. I also don’t think expecting a dev interested in Rust to understand several C-like languages is unreasonable, at least enough ti understand these straightforward example cases.
> Inner functions. Rust also supports inner functions.
...
> 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 saved up Common Lisp resources for a few years and in 2022 I finally decided to sit down and learn it. It was entirely worth it, so I recommend you sit down to learn Rust one weekend. In fact, do it next weekend. Getting started on anything is always better done sooner than later.
I can only justify Rust for hobby coding, none of the stuff I do professionaly cares about what Rust offers, compiled managed languages are good enough and have decades of software maturity, and using Rust as translation layer between them and C++ libraries hinders more than it helps.
With the caveat that syntax is the ultimate bikeshed topic, one (IMO) syntactic wart is the special pattern syntax:
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.
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.
Using clearly defined bit sizes (i32, f64) rather than legacy naming conventions (int, double) is a good idea, the language could be really quite something if they switch to S-expressions.
Syntax or semantics? Not a lot for syntax... maybe the "turbofish" syntax with generic types is a bit too much line noise: <Foo<_> as Bar<_>>::baz<_>()
I think the hard part is understanding how limited is basic feature set of just 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.
wrs|3 years ago
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...
scotty79|3 years ago
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.
coderenegade|3 years ago
I found it frustrating enough that I put the language down and just went back to using C++.
attractivechaos|3 years ago
csomar|3 years ago
You can ignore that in other languages, but this comes at a cost later.
insanitybit|3 years ago
aussiesnack|3 years ago
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.
xiphias2|3 years ago
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).
jackmott|3 years ago
[deleted]
mlindner|3 years ago
kika|3 years ago
heavyset_go|3 years ago
clumsysmurf|3 years ago
nine_k|3 years ago
(They seemingly don't use more apt comparisons with OCaml and Haskell, for instance, not expecting the reader to know them.)
86J8oyZv|3 years ago
deepsun|3 years ago
> 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.
joaquincabezas|3 years ago
I really hope to start using Rust in 2023, probably for some kind of API gateway experimentation
tmtvl|3 years ago
pjmlp|3 years ago
rr808|3 years ago
solomatov|3 years ago
ridiculous_fish|3 years ago
John23832|3 years ago
ww520|3 years ago
jason2323|3 years ago
skor|3 years ago
ridiculous_fish|3 years ago
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.
scotty79|3 years ago
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:
or 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:
or even or to indicate that 'a must outlive the struct.Then you could use it:
Maybe functions that create non-static struct might have & at the ends in their names.Like
but 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.
tmtvl|3 years ago
legerdemain|3 years ago
tomr75|3 years ago
scotty79|3 years ago
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.