1. 67
  1. 15

    Only one thing jumped out on me which I definitely didn’t agree:

    Rust makes it especially easy to refactor

    Specifically, extracting a piece of code into a separate function could actually be harder in Rust than in some other languages with type inference due to Rust’s treatment of any function as an API boundary and its insistence on spelling out all argument types explicitly. Sometimes the types of intermittent values could be rather unwieldy (cough iterator adapters cough).

    1. 25

      I think Rust is easier to refactor correctly. When refactoring something, if it compiles, it often works right away. Maybe it’s just me but when refactoring C++ or Python I always introduce little mismatches that I don’t find until runtime.

      1. 8

        Indeed, it’s all relative. I have found having a good strong type system can embolden me to do things I wouldn’t even attempt in a dynamic language. I don’t have first hand experience doing heavy refactoring in Rust; I’ve only used it for a few small projects, but this has been my experience with Haskell, OCaml, and heck even Go, when the type system is capable of covering the invariants you care about. I do find that for a lot of my big refactors in Haskell, a decent chunk of the time it does take has been sunk into updating type annotations that are in principle optional, so I think type inference would help here – but I’ve also found that in larger codebases having the types actually written down for most top-level functions makes reading the code easier. This seems like something a good IDE could solve for you, but I’ve never really taken to IDEs, and the languages that have good inference have not historically had good IDE support anyway (though it’s been improving).

        1. 2

          Tests work at runtime, have better covering tests :-)

          1. 6

            Why would I debug minor type mismatches like a peasant when the compiler can instantaneously tell me the exact location of the issue?

            Worse, when refactoring C++, changes can introduce subtle lifetime issues that even good tests may not trigger. ASAN can help, but then I’m debugging ASAN errors instead of the Rust compiler pointing me to the exact line of code causing a problem.

            I write good tests, and lots of them. But running a test suite is not a substitute for type and lifetime checking in my eyes.

            1. 4

              Indeed, my somewhat pathological case that I’ve come to point to is SQLite; the thoroughness of their testing practices borders on the absurd (which is a good thing):

              https://sqlite.org/testing.html

              the SQLite library consists of approximately 143.4 KSLOC of C code. […] By comparison, the project has 640 times as much test code and test scripts.

              And yet, memory safety bugs are found somwhat regularly:

              https://www.sqlite.org/cves.html#cvetab

              The page makes a good argument that most of these don’t really count as vulnerabilities, but they are still bugs that would be impossible to even get past the type checker in Rust, which somehow evaded what is the most rigorous testing regime I have ever heard of, on any project in any language.

              To be sure, there are many kinds of bugs in many domains that type checkers aren’t great at helping with, much less ruling out entirely. But there are also many where they are a great fit, and in those cases they are both more complete than any possible test suite and also much less effort to make use of.

              Neither testing nor type checkers should be the only tool in your toolkit.

        2. 5

          I like type inference within a function body, but global inference seems to invariably make debugging more difficult. Even if your editor shows you type annotations, these annotations are the most abstract types that the type system supports rather than the types a programmer intended. Moreover, it seems like managing compatibility would be a big pain if any change made inside of the function body would quietly change its signature (I’m sure tooling exists which could check for this, but I have a hard time imagining that tooling is more pleasant to work with than simple, by-human-for-human annotations).

          1. 4

            My experience is that I can feel free to start a big refactoring in Rust, even when I’m working on a new feature in parallel, because I feel secure: I won’t forget anything, the compiler is guiding me step by step and makes me converge towards the correct refactored code. It’s “fearless refactoring”.

            1. 2

              I agree with you, refactoring in Rust is quite painful. The author is not wrong about the compiler telling you what changes you need to make once you change a signature or a type, but as you say, I think is way harder that other languages…

              1. 1

                No, no, I didn’t imply it was painful in general. I just noticed this particular difficulty.

            2. 9

              Build immutable structures and collections

              Rust is (generally) referentially transparent, meaning that instances of ‘mutation’ can be trivially (locally) resugared. So except as a point of ergonomics this doesn’t seem like a particularly meaningful statement.

              Old CS courses are full of mathematically pretty algorithms or data structures that are used again and again in whiteboard or learning exercises.

              The most striking example is the linked list and its variations.

              Those structures are usually poorly effective in recent hardware

              This is an incredibly naive and simplistic view. The performance characteristics of modern hardware are incredibly complex, and broad declarations like ‘pointers are bad, arrays are good’ doesn’t help anyone.

              That rust is poor at such data structures reflects poorly on rust, not on those data structures. It limits the domains to which rust is applicable. (And this I say as a point of ergonomics, not performance.)

              This section (along with some of the others) almost reads like ‘rust is a people’s language, far from the artificial constraints of ivory-tower academia’. (Which is ironic, considering that most of the good parts of rust come from ocaml, an academic language; but it is not worth taking such attitudes seriously.)

              1. 9

                Rust exposes the fact that memory management for a linked list is hard. You can implement a linked list but satisfying the borrow checker while doing so is difficult. Many times you’ll just drop to some form of weak reference handle system backed by a vec. Which gives you the ergonomics of the linked list and less fighting the borrow checker.

                In languages where the linked list is easier there is either going to be a GC which gives you memory safety at the expense of some performance. Or there will be no memory safety and doing the linked list safely will be up to you with all of the pitfalls that entails.

                1. 4

                  GC which gives you memory safety at the expense of some performance

                  Bacon tells us that tracing garbage collectors operate on and attempt to find live objects, while reference counters operate on and attempt to find dead ones. If we consider rust’s single-ownership semantics to be a form of reference counting (where the reference count is maintained at 1 or 0 at all times), we may compare them under these terms. The generational hypothesis tells us that, if we are clever, we may consider a context in which there are many dead objects and few live ones, and so profit by considering only the live ones.

                  Practically, using (say) a semispace collector, the overhead of collecting a young graph is proportional to the number of live nodes; while a reference-counting implementation will also have to work on the dead ones.

                  Obviously there is overhead to such an approach; but your implicit premise that that overhead is greater than other approaches has not been justified.

                  drop to some form of weak reference handle system backed by a vec

                  If I wish to represent pointers using indices, I will use APL. If I wish to represent a collection of objects with related lifetimes, I will use a language with first-class support for regions. What you describe is not a natural or obvious pattern for a language such as rust, which already has typed pointers; it rather layers a new memory model atop rust’s, and one which is no more strongly typed than c’s. (I am sure if you put in even more effort you may make it strongly typed; but how likely are you to err in designing or implementing your new type system?)

                  As I said, the primary issue is one of ergonomics, not performance. Clearly correctness is more important than performance in all cases. If a graph is the right tool for some job, we should rather use a graph than attempting to shoehorn in some other structure. Rust should make graphs easy and natural to work with, even if it can not make them fast.

                  1. 1

                    Erratum:

                    we should rather use a graph than attempting to shoehorn in some other structure

                    ‘attempting’ should be ‘attempt’.

                  2. 3

                    This doesn’t explain how trivial things you can do with pointers and manual memory management in C, such as a cyclically linked list, doubly linked list or XOR linked list, become “hard” in Rust, in that the borrow checker will refuse to accept it (I’m calling it trivial because I think it is subjective and the fact is rather that the borrow checker is unhelpful in such cases).

                    Alternative explanation: There are just many things that the borrow checker doesn’t know how to verify, because it isn’t a generic proof checker. It only knows one good special case – when everything has one owner. When this isn’t the case, there is no way to satisfy it (other than wrapping the code in unsafe). To someone who doesn’t expect this, this presents an unwinnable fight with the borrow checker.

                    The Rust-specific “problem” then becomes that of writing the least amount of unsafe code, and that’s why it’s not for beginners. But the problem, if you care, obviously isn’t compared to having everything in a language without a borrow checker. I can’t say I’ve had to write any unsafe Rust yet, but I would expect it to be as easy as C once you know that you need to.

                    1. 7

                      I think that people who have gotten really comfortable with C underestimate how hard the manual memory management juggling becomes when there is more than one owner of something. I stand by my statement that what the Rust borrow checker is demonstrating is that this is in fact not as easy as it sounds to do safely. Trivial one file examples in C don’t invalidate the statement that if you are working with linked list in a larger system it can be easy to end up with dangling pointers, use after free errors, and other issues caused by the fact that there are more than one owner of something.

                      1. 3

                        It’s a lot easier to write these sorts of data structures in Rust if you use raw pointers and unsafe blocks to dereference them. Which puts you in a position similar to C, except you’re more conscious of the possibility of memory safety bugs because you had to type unsafe a lot.

                        1. 2

                          This is true, but it does give misleading impression, because “as if” rule applies. Borrow checker requires everything acts as if it has one owner, not everything has one owner. The former is, in practice, much more general. For example, borrow checker works fine with reference counting, but reference counted objects do not have one owner. If they have one owner, they are owned by “reference counting”, which is rather metaphorical.

                      2. 1

                        This section (along with some of the others) almost reads like ‘rust is a people’s language, far from the artificial constraints of ivory-tower academia’. Which is ironic, considering that most of the good parts of rust come from ocaml, an academic language; but it is not worth taking such attitudes seriously.

                        But this isn’t irony—there’s no conflict at all, except that you assume the people who like Rust for its pragmatism would agree with you that the “good parts” of Rust are it’s more academic features rather than its fundamentals. To your point, the academic features are more celebrated, but to the pragmatist they’re gravy. I can build great, successful software with “lesser” type systems but it’s much harder in a language with an advanced type system but poor tooling, fragmented standard library, off-putting or hard-to-read syntax, and/or impoverished ecosystem. In my opinion, “the good parts” of Rust are its pragmatic features—the academic ones are icing on the cake.

                      3. 7

                        Good general advices… As a new learner myself the one mistatke that really should be mentioned everywhere is… Start by implementing a graph based algorithm. I mean, all the other advices are good, and I’ve done all of them, but that one is the one that make almost let Rust aside.

                        1. 3

                          Mistake 7 : Build lifetime heavy API

                          I’ve struggled a bit with this. More generally, as a newcomer to Rust, I’ve found myself acting as if the slightest compromise in efficiency is a slippery slope straight to the excesses of Electron. Rust promises zero-overhead abstractions, so sometimes I try too hard to use the zero-overhead solution for everything. Another example is trying to make a struct or function generic instead of just using Box<dyn MyTrait> or even Arc<dyn MyTrait>.

                          1. 3

                            As someone who has been using rust for a long time, I do the same.

                            Not because I think it’s a good idea, but because it’s just too much damn fun to try and do it “optimally”.

                            1. 1

                              Well, I always never use dyn. Generics are fun to use when you understood the basics.

                              Lifetime heavy API are another beast because they often reduce the usability of your API, especially when you make your users deal with a lot of structs, none of them owning the data.

                            2. 3

                              Start by implementing a graph based algorithm.

                              My favorite personal projects are all intrinsically graph-based. Is Rust a poor fit for me?

                              1. 3

                                Use something like petgraph you will be fine. Or if you’re implementing it by hand, store the nodes in an arena/vec and instead of pointers use indicies.

                                It’s a bit of a bigger step for newcomers, because you’re learning both “rust” and “how to deal with graphs in rust without fighting the borrow checker” at the same time, instead of one and then the other.

                                1. 1

                                  The takeaway message here isn’t “graphs are impossible, or even particularly hard to do in Rust (once you know Rust)”.

                                  It’s that Rust makes you think very explicitly about the hardest part of graph programming (provably avoiding dangling pointers in all cases, in all codepaths), and that trying to work at this level of explicitness in a language you haven’t actually learned yet is an easy way to self-sabotage the learning process.

                                  Learn Rust. Learn lifetimes and ownership and how those things interrelate to allow for correct memory management, and how Rust deals with the heap and runtime ways of enforcing ownership rules, etc. Learn about strategies like arena allocation for when you want children to be able to point at their parents without owning their memory. Then build your fun graph projects.

                                  Just don’t try to walk and chew bubblegum at the same time when you’re not even crawling yet.

                                  1. 1

                                    Same for me and I still manage to write them. Simply be sure to understand that you’ll have to suffer more than other people learning Rust if you start here. The best working approach is the arena+index one but it needs you to be very cautious (test units help, of course) as it means you’re going around the protection given by the ownership model.

                                  2. 3

                                    With respect to linked lists, a colleague referred me to https://rust-unofficial.github.io/too-many-lists/ when I was learning, and I found it a very good introduction, covering the intricacies of the various lifetime and related issues.

                                    1. 2

                                      The most striking example is the linked list and its variations. I always see newbies come to Stack Overflow and ask how to write them. Those structures are usually poorly effective in recent hardware (and the instruction count of big O notation doesn’t tell you how much) hard to do in Rust

                                      Hilarious.

                                      1. 1

                                        Above average. Insightful. Recommended.