1. 19
  1. 4

    All these compiler errors make me worry that refactoring anything reasonably large will get brutal and demoralizing fast. Does anyone have any experience here?

    1. 20

      I’ve got lots of experience refactoring very large rust codebases and I find it to be the opposite. I’m sure it helps that I’ve internalized a lot of the rules, so most of the errors I’m expecting, but even earlier in my rust use I never found it to be demoralizing. Really, I find it rather freeing. I don’t have to think about every last thing that a change might affect, I just make the change and use the list of errors as my todo list.

      1. 6

        That’s my experience as well. Sometimes it’s a bit inconvenient because you need to update everything to get it to compile (can’t just test an isolated part that you updated) but the confidence it gives me when refactoring that I updated everything is totally worth it.

      2. 9

        In my experience (more with OCaml, but they’re close), errors are helpful because they tell you what places in the code are affected by the refactoring. The ideal scenario is one where you make the initial change, then fix all the places that the compilers errors at, and when you’re done it all works again. If you used the type system to its best this scenario can actually happen in practice!

        1. 4

          I definitely agree. Lots of great compiler errors make refactoring a joy. I somewhat recently wanted to add line+col numbers to my error messages and simply made the breaking change of defining the location field on my error type, then fixed compile errors for about 6h. When the code compiled for the first time it worked! (save a couple of off-by-one errors) I have to say that it is so powerful that you can trust the compiler to let you know the places that you need to make changes when doing a refactoring, and catching a lot of other errors that you may make as you quickly rip through the codebase. (For example even if you get similar errors for the missing arguments in C++ quickly jumping to random places in the codebase makes it easy to introduce lifetime issues as you don’t always successfully grasp the lifetime constraints of the surrounding code as quickly as you think you have.) It is definitely wat nicer than dynamic languages where you get hundreds of rest failures and have to map those back to the actual location where the problem occured.

        2. 7

          In my experience refactoring is one of the strong points of Rust. I can “break” my code anywhere I need it (e.g. make a field optional, or remove a method, etc.), and then follow the errors until it works again. It sure beats finding undefined is not a function at run time instead.

          The compiler takes care to avoid displaying multiple redundant errors that have the same root cause. The auto-fix suggestions are usually correct. Rust-analyzer’s refactoring actions are getting pretty good too.

          1. 3

            Yes. My favourite is when a widely-used struct suddenly gains a generic parameter and there are now a hundred function signatures and trait bounds that need updating, along with possibly infecting any other structs that contained it. CLion has some useful refactoring tools but it can only take you so far. I don’t mean to merely whinge - it’s all a trade-off. The requirement for functions to fully specify types permits some pretty magical type inference within function bodies. As sibling says, you just treat it as a todo list and you can be reasonably sure it will work when you’re done.

            1. 2

              I think generics are kind of overused in rust tbh.

            2. 2

              I just pick one error at a time and fix them. Usually its best to comment out as much broken code as possible until you get a clean compile then work one at a time.

              It is a grind, but once you finish, the code usually works immediately with few if any problems.

              1. 2

                No it makes refactors much better. Part of the reason my coworkers like Rust is because we can change our minds later.

                All those compile errors would be runtime exceptions or race conditions or other issues that fly under the radar in a different language. You want the errors. Some experience is involved in learning how to grease the rails on a refactor and set the compiler up to create a checklist for you. My default strategy is striking the root by changing the core datatype or function and fixing all the code that broke as a result.

                1. 1

                  As a counterpoint to what most people are saying here…

                  In theory the refactoring is “fine”. But the lack of a GC (meaning that object lifetimes are a core part of the code), combined with the relatively few tools you have to nicely monkeypatch things mean that “trying out” a code change is a lost more costly than, say, in Python (where you can throw a descriptor onto an object to try out some new functionality quickly, for example).

                  I think this is alleviated when you use traits well, but raw structs are a bit of a pain in the butt. I think this is mostly limited to modifying underlying structures though, and when refactoring functions etc, I’ve found it to be a breeze (and like people say, error messages make it easier to find the refactoring points).

                2. 1

                  At this point, the compiler can correctly infer the lifetime, and we can remove all hints.

                  Did this strike anyone else as odd? I’m not on my computer so it’s hard to visually diff the starting and ending code, but I can’t see any differences.

                  1. 4

                    If I’m understanding which bit you’re referring to, the difference is:

                    pub fn find_largest_group<’a>(groups: &’a Vec) -> Option<&’a Group>

                    vs

                    pub fn find_largest_group(groups: &Vec) -> Option<&Group> {

                    It’s not the most obvious in a fairly noisy bit of code.

                    1. 1

                      You got it, that’s the part - in “Implementing Our First Function.” However there’s just one more code snippet under the text that I quoted and that is remarkably like the original code, even on desktop. So that section starts with:

                      pub fn find_largest_group(groups: Vec<Group>) -> Option<&Group>

                      And ends with:

                      pub fn find_largest_group(groups: &Vec<Group>) -> Option<&Group>

                      (diff here)

                      I was just surprised because the text seems to imply that you can give the compiler hints until it knows what you mean and then take those hints away, which I was reasonably sure wasn’t how the compiler works, even as a Rust noob. I’ll write to the author to clarify.

                      1. 4

                        The compiler took the user for an unnecessary ride via 'static lifetime annotation, which in 99.99% of cases is not applicable. I believe that bad suggestion bug has been recently fixed. Unfortunately, the compiler is still not smart enough yet to offer the best suggestion to use &[Group] instead.

                        1. 2

                          the text seems to imply that you can give the compiler hints until it knows what you mean and then take those hints away, which I was reasonably sure wasn’t how the compiler works

                          You are right, the compiler doesn’t work that way. They could have added the single & in one shot and it would have worked. I read it as the author describing their thought process, but was thinking to myself that this wording is going to confuse people.