Rust for Professionals

126 points by keewee7 2 years ago | 100 comments
  • wrs 2 years ago
    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.

    For the C++ comparison, this is a pretty good article: https://radekvit.medium.com/move-semantics-in-c-and-rust-the...

    • scotty79 2 years ago
      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.

      • alltheworlds 2 years ago
        As someone who hasn't looked at Rust, reading all this makes me never want to. Am I missing something? Is it actually easier to reason about the language than it feels like from reading this text?
        • zozbot234 2 years ago
          This is just stuff that's flagged automatically by the compiler. You don't need to "reason" about it, the compiler will tell you how to fix it if it does come up.
      • coderenegade 2 years ago
        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++.

        • scotty79 2 years ago
          > In C++, move semantics are obvious because ...

          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...

          • legerdemain 2 years ago

              > In Rust it's also obvious because every = is a move.
            
            False, depending on the presence of an explicit type annotation on the left-hand side, an = triggers either a move or a reborrow.
          • saghm 2 years ago
            I'd be pretty surprised if the C++ move semantics were considered more intuitive by someone not familiar with either and then learning both for the same time. Rust's semantics do depend on the type in that anything that implements Copy will be implicitly copied rather than moved, but I'm sure I understand why that's unintuitive. I'm also not sure what you mean by Rust having it's own move operator; the only thing I can think of is the `move` keyword used for indicating that closures should capture by move rather than by reference, but it's not used outside of closures as far as I'm aware, so I suspect that the confusion here is more due to expecting things to behave like C++ rather than the Rust semantics being "unintuitive" in a vacuum.

            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.

      • attractivechaos 2 years ago
        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.
        • csomar 2 years ago
          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.

          • legerdemain 2 years ago
            My experience with Rust data structures is the opposite. To satisfy lifetime analysis inherent in pointer-based data structures, users tend to introduce additional indirection. For example, mapping keys to indexes, and then indexes to array elements, instead of simply mapping keys to values.
            • verdagon 2 years ago
              I've used Rust quite a bit, but I would love to hear more about this. Is this comparing to lower level languages like C/C++/Zig, or higher level GC'd languages? What problems does it make us think more about up-front, and do you think it's due to single ownership (in the C++ and Rust sense) or the borrow checker?
              • csomar 2 years ago
                If you think about programming as information transformation (ie: you are getting information in, applying a certain transformation and then giving some output), programming becomes about how this information flows. Data structure becomes the most important thing.

                Here is an example that now frustrates me about JavaScript. I don't know if some variables "MyVal" declared somewhere is global, local or how transfer it around. Global variables no longer makes sense to me. In Rust, global variables are possible but they are obvious and their scope is completely within your control. Now, all the data is there and it's up to you how to structure it, move it around, store it, etc.. You have to think these through before doing any code. If you don't think well enough, the Rust compiler (borrow/ownership thing) will start to give you headaches.

            • insanitybit 2 years ago
              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()'.
              • zozbot234 2 years ago
                You can also use Cow<> to choose between cloning and and immutable borrowing at runtime.
                • insanitybit 2 years ago
                  Unless you're using `'static` you will still run into lifetimes with Cow, which I suspect a lot of people will find difficult. I'd suggest just using clone, learning the broader language, and learning about lifetime stuff later.

                  Lifetimes aren't hard or complicated, what's complicated is understand their interaction with other features (closures) when you don't even understand wtf closures in rust are, or traits, etc. I'd just focus on the other stuff for as long as you can.

              • aussiesnack 2 years ago
                But it's so easy to get a handle on how the borrow checker works! Just check out this thread and all will become clear: https://users.rust-lang.org/t/what-is-a-good-mental-model-of...

                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.

                • insanitybit 2 years ago
                  I mean, some question. I learned Rust with 0 professional experience in any language (I learned it after I dropped out of a CS program after 2.5 years) and I found it pretty damn easy. That was in 2015 when the language was wayyyy less approachable (worse borrow checker, everything used nightly, smaller community, no book).

                  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.

                  • aussiesnack 2 years ago
                    > Easy is relative

                    In the case of programming languages, yes, it's relative to the difficulty of other PLs. I've learned many over the years, and found Rust by far the hardest (it's the only one that defeated me). And it's not the most different from others I've learned - lisps are far further from the common languages than Rust is.

                    > I suspect a major reason I found it easy was because I didn't try to solve lifetime problems,

                    Well yes anything's easy if you skip the hard bits. Learn C without using pointers.

                    I personally didn't find ownership & borrows the hardest part - in my case it's the great complexity of many of the commonly used libraries. Rust's complexity bleeds out into the entire ecosystem (a cultural thing).

              • xiphias2 2 years ago
                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).

              • mlindner 2 years ago
                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.
                • kika 2 years ago
                  My experience with Java ended circa 2000 and I never wrote a single line in Kotlin. But I read these examples without noticeable issues.
                  • andsoitis 2 years ago
                    what have you been programming in the past 20 years?
                    • kika 2 years ago
                      In approximate chronological order: Python, Lisp, C++ (ATL and Qt), Erlang, JavaScript, TypeScript, PureScript, Rust.
                  • heavyset_go 2 years ago
                    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.
                    • clumsysmurf 2 years ago
                      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++ }
                      • nine_k 2 years ago
                        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.)

                        • mlindner 2 years ago
                          Javascript I've seen a decent amount and tweaked/edited some of code in it but I wouldn't say I "know" it at all. Kotlin I've never even seen before and I know literally nothing about the language. Java I wrote a bit of in high school but haven't touched it in 15 years.

                          So yeah there's quite a lot that people wouldn't know.

                          • nine_k 2 years ago
                            Well, Golang? Pascal? They have similar ways of declaring data types, for instance.

                            This guide is not going to magically teach you Rust is there's nothing to compare it to; there are other guides to help those without a background. But it could somehow help people who already have similar concepts in their mind to link them to corresponding concepts in Rust.

                            • Jtsummers 2 years ago
                              You've said what languages you don't know, what languages do you know?
                          • 86J8oyZv 2 years ago
                            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.
                          • deepsun 2 years ago
                            > 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.

                            • joaquincabezas 2 years ago
                              I keep saving these Rust resources for a near future... Am i the only one??

                              I really hope to start using Rust in 2023, probably for some kind of API gateway experimentation

                              • tmtvl 2 years ago
                                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.
                                • kika 2 years ago
                                  joaquincabezas, don't listen to this person. It's a trap. In February I found myself in AirBnB alone with nothing to do, because my wife had to stay back home for an extra week and I waited for her to join me. AirBnB had a decent work desk, decent(-ish) monitor and barely ok keyboard so I decided to learn Rust. Now it's January, I'm at 3000+ LoC of Rust and about 4000 in Dart/Flutter and trying to make the project to ShowHN. Weekend project, my ass. Rust is highly addictive, you've been warned. I tried to get sober, left this project for months on end, but always relapsed.
                                  • joaquincabezas 2 years ago
                                    fn main() { println!("Too late I guess!"); }
                                  • randmeerkat 2 years ago
                                    > Getting started on anything is always better done sooner than later.

                                    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.

                                    • avaldez_ 2 years ago
                                      Nice. I'm doing exactly that with CL, one of my new year's resolutions. Would you mind to share your resources?
                                      • tmtvl 2 years ago
                                        Besides the obvious Big 4 (Gentle Intro, PAIP, On Lisp, and Cookbook) I am also quite fond of Lisp in Small Pieces. Aside from those I'd also recommend grabbing the GCL source and building the Info manual so you can browse it in Emacs, it contains a fairly complete copy of the HyperSpec (I know it's available online, but I don't like visiting websites that haven't updated to HTTPS yet).

                                        Also check out CLiki (the Common Lisp wiki, https://www.cliki.net/), it's very helpful in finding useful libraries, like Alexandria, defstar, trivia, lparallel, and so on.

                                    • pjmlp 2 years ago
                                      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.
                                      • rr808 2 years ago
                                        Yeah I wish there was a strong jobs market for Rust developers then I'd feel like I wasn't wasting my time.
                                        • Gigachad 2 years ago
                                          There aren't many full Rust jobs but there are a whole lot of companies with ever expanding bits of Rust in production. Probably the best way at this point is to push it internally and put together a convincing case for using Rust for new projects.
                                          • alltheworlds 2 years ago
                                            This is our company. Frankly, I don't agree with this approach. We're a hedge fund that uses Java for low latency trading systems. Someone pushed for Rust to be used for a new project, because they wanted to use it, and now we have a split codebase. We have duplicate code (eg. connections to external APIs) written in Java and Rust, and now we have two sets of code to maintain whenever one of those APIs change.

                                            I always get annoyed when I hear about people suggesting to push for a language in a company because they feel it would be a good fit. Sure, maybe it would be, but now the company will always require experts not in one language but two, and developers are no longer as fungible between teams.

                                            Having said that, from a selfish perspective, I will use it to write some production Rust code in the future.

                                          • bschwindHN 2 years ago
                                            What kind of Rust job would you be looking for?
                                        • solomatov 2 years ago
                                          This document only briefly mentions interior mutability, which IMO, is one of the most important things to become productive in Rust.
                                          • ridiculous_fish 2 years ago
                                            Curious why the author chose not to discuss macros? You encounter them immediately with a Hello World.
                                            • John23832 2 years ago
                                              Declarative macros would be too confusing to get into in any way other than "the ! in println! denotes a macro".
                                            • ww520 2 years ago
                                              This is an excellent short tutorial. It helps to compare and contrast to other languages.
                                              • jason2323 2 years ago
                                                As someone coming from a Java background, this seems useful! Thank you!
                                                • skor 2 years ago
                                                  any constructive criticism on rust syntax?
                                                  • ridiculous_fish 2 years ago
                                                    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.

                                                    • scotty79 2 years ago
                                                      > 2.

                                                      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.

                                                    • scotty79 2 years ago
                                                      I don't like how

                                                      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.

                                                      • tmtvl 2 years ago
                                                        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.
                                                        • legerdemain 2 years ago
                                                          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<_>()
                                                          • guilhas 2 years ago
                                                            Definitely, rust looks clear for what is programmatically happening, making it noisier to overview the problem being solved
                                                        • tomr75 2 years ago
                                                          the hardest part and barrier are the concepts behind lifetimes/ownership/borrowing not the syntax
                                                          • scotty79 2 years ago
                                                            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.