> In some sense they're inevitable but my early ignorance exacerbated the problem: I'd clone to convince the borrow checker to let me move on, would construct new containers rather than pass new ones in to be refilled and the like. Also, strings. Lots and lots of strings everywhere.
I've been teaching myself Rust in the evenings for a couple of weeks now and have found the process maddeningly confusing and slow. Even with the Book, which is fine, I ended up getting into rage induced clone/to_owned/etc keyboard mashing dances with the borrow checker. If it weren't so easy to do this kind of dev with emacs, I might have given up earlier.
On the other hand I occasionally get these moments where my code just "clicks" and it feels good and _seems_ elegant. But, pedagogically speaking, at this point I'm just trying to learn through immersion, not through theory and understanding.
Honestly I'd probably be way done with this little project if I had started in Go, but I think I'm going to stick with Rust for a while.
> I still fought the borrow checker after a year of using Rust.
That's interesting. How much Rust did you use during that year? And how level of proficiency did you reach?
> And I found many situations while using Rust where I either needed to clone, or use unsafe
That sounds like an uncommon trade-off. From my experience, the trade-off was usually “clone or put lifetime parameters everywhere”, which is annoying (and I really wish Rust analyzer could help with at some point, because it's pretty mechanical), but it was quite rare, because most of the time you can just move things (that is transferring ownership), unlike in C where you have either a copy or a pointer.
> or use unsafe where it's easier to make a mistake than in other languages because the syntax is extremely unergonomic and the memory semantics are much less clear.
This is unfortunately true. Miri helps, but there's a wide margin for improvement on that front.
>Rust, I think, suffers from a lack of empathy with a novice audience.
The borrow checker is the simplest thing once you internalize it's rules. Just the road to breaking all the bad habits C/C++ teach you are fine over the years takes a while. And most these rules are mostly excessive and unnecessary coming from a C perspective. The borrow checker is kind of like type systems. It prevents a whole class of errors, but you lose the ability to express some perfectly valid code.
It is really a kind of mental-quantum-leap. Once you internalize it's rules you really lose the ability to understand how other people can't.
Also to deflect some blame from The Rust-Community. I'm just kind of a horrible person on the internet. I'm working on improving :\ Sorry for the coarse comment.
> Again, maybe it is just me and need to spend more time learning it.
I had the exact same experience : after my first reading of the Rust book, I though I understood what's going on but once I tried to actually write something I was totally lost. I stopped using Rust for several months and when I came back, I read the book again and this time I had no troubles writing stuff and the concepts of ownership and lifetime became really clear.
I think the concepts behind the borrow-checker are really straightforward[] once you've internalized them, yet they are really unintuitive to the beginner. It's quite paradoxical.
> This arguing with the compiler is really only while you are learning Rust and it's nuances. For me the first two weeks were this way, then I started thinking more in Rust terms. Now I rarely deal with the borrow checker at all, but I do end up in some complex type situations.
It just means you've learned to head off problems before they occur, that does not mean the problems you're heading off should be there.
IMvHO, Rust Ownership and Lifetime rules aren't really that hard to learn. The only trick is that a programmer cannot learn Rust exclusively by trial-and-error (which programmers love to do), trying dozens of syntax combinations to see what works. A programmer is forced to learn Rust by RTFM (which programmers hate to do), in order to understand how Rust works. That's all there's to it. It's like trying to learn to play the guitar without reading a book on guitar cords, pulling guitar strings individually at random - the music just won't happen for quite a long time, unless you're Mozart. At any rate, one can always forget Rust references (i.e., raw pointers) and use Rust reference-counted smart pointers[1], effectively writing Rust analogously to Swift.
> A lot of it is just how restrictive the borrow checker is. Often you have to structure your code in a really weird way to satisfy it.
I've rarely found this the case for me but I came from C++ and am generally used to thinking in terms of lifetimes. Could you elaborate on how you ran into problems with borrows messing up code structure?
> Dealing with strings is another pain point. I get why there is `&str` and `String`. But that doesn't explain why I can't add two `String`s together using +.
I feel like the Rust stdlib takes after C++ in making costs obvious to people and not providing shortcuts for slow operations, causing the ergonomics to scale with performance.
Something that some of us in the CLI-WG have been talking about is creating a library that interops with the standard library but instead focuses on python-like semantics at the cost of performance. This will be a big benefit for "quick scripts" and people learning the language while allowing people to still fall back to high performance idioms as determined by a profiler.
Unfortunately, this is a little lower on our priority list for now.
> The most fundamental issue is that the borrow checker forces a refactor at the most inconvenient times. Rust users consider this to be a positive, because it makes them "write good code", but the more time I spend with the language the more I doubt how much of this is true. Good code is written by iterating on an idea and trying things out, and while the borrow checker can force more iterations, that does not mean that this is a desirable way to write code. I've often found that being unable to just move on for now and solve my problem and fix it later was what was truly hurting my ability to write good code.
The latter part of this is true for any strongly statically typed language (with Rust expanding this to lifetimes), which negates the beginning of this paragraph -- once you get things compiled, you won't need to refactor, unless you are changing major parts of your interfaces. There are plenty of languages that do not have this problem because it is a design choice, hardly something Rust can "fix", it's what makes it Rust.
> My diktat is "clone is banned unless you can explain why you must have it", and then I teach people about what a move is and how to think about lifetimes.
That is good advice for people that are getting familiar with the borrow checker, making them think about allocations and ownership, but making newcomers that are getting familiar with the entire language, in some cases coming from very different paradigms, can be very demoralizing and the reason they stop or become convinced that "Rust is too hard for them" when what is happening is that they are trying to learn too many concepts at the same time.
The way I see it, the learning curve for most of Rust is a fairly mild slope, with a climbing wall around lifetimes. The further you progress learning the rest of the language, the shorter the wall will feel.
That being said this is born of my experience helping a few people learning Rust, but I could be completely off-base for the general case.
> w.r.t. Arc/Rc, I'd say that it might feel verbose at first, but it makes explicit what a GC does implicitly, and you can get all the benefits of Rust's ownership model at the same time.
But it is verbose. I think this is part of the problem with Rust learnability, because Rust makes inefficient code evident (think unsafe, clone and Rc), and that makes experienced programmers want to remove the inefficiency before they are proficient with the language enough to do so, so they encounter the hardest edges of the language early.
I appreciate that these markers make it better for me when reading the code and I wouldn't want them to disappear, but it does make it for a more verbose experience where the compiler sometimes feels pedantic. I think that better refactoring tools could make these kind of pains (and related ones, like adding lifetimes to a struct) go away almost entirely.
> I can pick and choose when to rely on the GC and when to explicitly manage lifetimes myself. That's really cool! I maybe a bit weird in this, but I like complexity to get surfaced so I can't pretend it's not there.
I'm in the same boat. I just wish it was easier for rustc to detect early when you're trying to apply a pattern from a language that doesn't have memory ownership or thread safety or relies on internal mutability and provide appropriate advice beyond "you can't do that".
> I think the use case is "I haven't yet completely grokked the borrow checker and/or I don't have time to pacify it, but I would prefer not to copy potentially large data structures all over with Clone".
For basic stuff I agree, though I wouldn't use it.
> about the Rust community prescribing this as a solution to newcomers.
There's probably a sweet spot for using those constructs in not so obvious places while going through the simpler stuff in a more idiomatic way
"As the compiler will stop you from borrow checking or something unsafe again and again, you are being distracted constantly by focusing on the language itself instead of the problem you are solving. I know the friction is greater because I am still a Rust learner, not a veteran, but I think this experience stops a lot of new comers, speaking as someone who already conquered the uncomfortable syntax of Rust, coming from a C/C++ background."
To be honest, this article seems somewhat ingenuous. The author wasn't comfortable with Rust, and it seems like he missed the point of the borrow checker. This article will now be circulated whenever Rust is discussed.
I've been writing Rust full time for the last month and a half or so and the language is a joy to use. The main difference is that the code I write feels very sturdy and permanent. I've heard other people say the same thing.
> It seems true to me that working with the Rust borrow checker is eventually fruitful, but there is a micro-case where working with lifetimes seems like a lot mental overhead with little gain, compared to a built-in garbage collector available in Lisp.
I fully agree. In the past year I have worked in Rust a lot and while quarrels with the borrow checker have definitely gotten more and more rare, they still happen occasionally. It's not that the borrow checker presents a reason which I think is incorrect or invalid, it's rather that it's sometimes difficult or just tedious to encode the proof that what you're doing is actually fine.
That said, there have been situations in which I was absolutely confident my code would have been fine if it weren't for the meddling borrow checker, but it actually wasn't and it helped me catch bugs.
Fortunately there is a bit of an escape hatch: if you want to, you can just start reference counting and get close to the experience you'd have with a proper GC. For example, there's nothing preventing you from implementing a reference counted linked list and writing
> but in order to manage memory the way the borrow checker wants, the programmer is at best tempted, and at worst forced, to add complexity to the program.
This is a problem for newcomers because they have to adapt their way of thinking into Rust way of thinking. Once one develops a set of patterns and principles this is not an issue.
Companies who have large codebases in Rust tend to have praise for it enabling solutions that are much easier to reason with and work on then if they were developed in C++.
It is obvious the author has little to no experience in developing production software in Rust.
>Either way, it feels like part of the learning curve is learning the "Rustic" way to do something - in general, you probably shouldn't be reaching for "unsafe" until you know what you're doing! ;)
I tend to agree with this sentiment. The moment I saw that OP was doing pointer arithmetic: I knew they were in for a bad time. It's not that you can't do it -- it's moreso that Rust's raison d'etre is to highlight the flaws inherent in that approach.
I hadn't thought about it, but I think you're right, my background in managed languages is probably why I didn't run into these sorts of issues until after I was already comfortable with Rust. I wasn't trying to make maximally storage-efficient collections right out of the gate, instead I was building application-level stuff on top of `std::collections`.
> rust demands that I cross every last t before I can run it at all.
It's worse than that IMO. Rust makes it very awkward/impractical to have cyclic data structures, which are necessary to write a lot of useful programs. The Rust fans will quickly jump in and tell you that if you need cycles, your program is wrong and you're just not a good enough programmer, but Maybe it's just that the Rust borrow checker is too limited and primitive, and it really just gets in the way sometimes.
Some of the restrictions of the Rust borrow checker and type system are arbitrary. They're there because Rust currently can't do better. They're not the gospel, they aren't necessarily inherent property that must always be satisfied for a program to be bug free. The Rust notion of safety is not an absolute. It's a compromise, and a really annoying, tiresome drain on motivation and productivity sometimes.
> Usually, you don't start learning a new language with multithreaded operations..
When I have started with rust I was not writing stuff with Box/Rc/Arc... or even explicit lifetimes. Not saying that I was cloning everything but for simple stuff you can come long way with just moving and simple borrowing.
>The borrow checker creates a new paradigm entirely
I don't think this can be emphasized enough. You can get deceivingly far in rust with an OOP style, only to come to the disheartening conclusion that it's impossible to do what you want with the architecture you spent months of work setting up.
For example, how would you architect a simple single threaded emulator? The CPU is an object, RAM is an object, easy enough. Okay, the CPU needs mutable access to RAM at all times, so we'll make the CPU own the RAM, sure why not? Okay now we want to implement a graphics device that can mutably access RAM, so we just...put the ram out on it's own and make it a Rc<RefCell<T>>?!? Now it takes three lines of code to unwrap all the smart pointers to write a single byte of memory?!? And it's slower because we have to pay for runtime borrow checking!
I went down exactly this road. I still use rust and love many things about it, but I've come to the conclusion that writing typical single threaded object oriented code in rust only looks like it works; it's an awful idea in practice.
I think the reason C++ people have so much trouble with rust is the syntax makes it easy to do the things you've always done, but the mysterious borrowchecker tells you no later on.
> My theory is that this removes direct understanding what happens and instead shifts the focus on how to get the task done while not violating the type system rules
In my experience it is in fact still a requirement to understand
what's happening in order to write clean and fast Rust code. It
is possible to satisfy the compiler using a lot of clone() calls
and similar shortcuts, but once you understand what data
structure is allocated when and where and how the whole
ownership principle works in practice you can suddenly avoid a
ton of unnecessary heap allocations and other pitfalls. I'd say
my experience with Rust improved a lot once I started grasping
what's happening in memory and why certain syntax elements
exist, as this makes fixing compiler errors more intuitive and
less a blind trial and error session with the borrow checker.
Some of Rust's syntax can look weirdly unfamiliar at the
beginning, but everything is there for a good reason. Nothing is
done implicitly, and much of the pattern matching and
Option/Result related stuff is Rust's way of replacing the
concept of `null` with thoroughly type-checked expressions.
> much of the complexity I’d unconsciously attributed to the domain — “this is what systems programming is like” — was in fact a consequence of deliberate Rust design decisions.
This is something I've thought a lot about as I've started to reach the "hundreds of hours" of working with Rust mark. Besides attributing the complexity of Rust to the domain of systems programming, I think a lot of Rust's complexity often gets attributed as a trade-of you're making for the safety guarantees afforded by the borrow checker. But I think a lot of the complexity in Rust is not related to the borrow checker at all, and is just a result of certain design decisions in the language.
For instance, taking the module system as an example, in general, I can declare a dependency in mu `Cargo.toml` file like this: `"crate_name"`, and then import it into a given source file using the use declaration: `use crate_name`. However there's a special case, where if the crate name uses dashes, like `"crate-name"`, then the compiler will implicitly resolve to that from a use declaration using underscores: `use crate_name`.
Similarly, if wade into an unfamiliar code-base, and I see a use declaration like this: `use path::to::foo", if I want to look for the code for this, it could be in one of three places:
1. The file `src/path/to/foo.rs`
2. The file `src/path/to/foo/mod.rs`
3. Some other file, based on re-export via a `pub use` declaration.
So in order to use Rust effectively, I have to just sort of know about all these implicit behaviors of the compiler, and in my experience it took months to learn enough of these tricks and corners to really just be able to sit down with an idea and start coding in Rust without consulting documentation and examples regularly. And even after that the compiler still surprises me sometimes.
To give another example of somewhat vexing implicit behavior which does relate to the ownership system, just today I had a block of code which looked like this:
let x = some_value;
match x { ... }
x = some_other_value;
match x { ... }
Which compiled fine. And then by commenting out the second assignment of `x`:
let x = some_value;
match x { ... }
// x = some_other_value;
match x { ... } // <-- use after move
suddenly I had an ownership error, because `x` was moved by the first match statement. So here what was really going on is that the compiler was "helping me" using implicit rules to establish that `x` was referring to a different memory location after the assignment. It's an example of how Rust has all these implicit behaviors and overlaid systems which are intended to make working in a borrow-checked context easier, but in practice what this often means is that when you change something in such a way that one of these implicit systems breaks down, it can cause a failure in what seems like a totally unrelated place, which can be very surprising.
I wonder if part of this has to do with the fact that Rust seems to appeal to a certain type of programmer who is attracted to esoteric topics and arcane knowledge, so the fact that Rust is essentially an unlimited well of complexity is more a feature than a bug to them. But I have been thinking a lot about what a programming language would look like which has an ownership system like Rust, algebraic types, and a trait system, but puts a ruthless emphasis on productivity and eschewing complexity.
> Just making borrow checker violations into warnings for debug builds would go a long way to making the language more tolerable to the overwhelming majority of programmers not already committed to it.
The problem is that the borrow checker isn't just a verifier of correctness. It actually impacts the semantics of a program.
For example, in javascript if you open a TCP connection and let your connection handle go out of scope, the connection will stay open. And as a result, your program will sort of just hang. In Rust, when you open a connection and your handle goes out of scope, the socket is closed automatically. How does the compiler know when your program should close the socket? It knows based on the borrow checker's analysis of your program.
If your program confuses the borrow checker, that means that the compiler can't figure out when it should close that TCP connection on your behalf. This isn't the sort of thing you can just turn off in debug mode, because programs should act the same in debug and release modes. A program which doesn't close the TCP connection is a different program from one which does.
There are ways around this - you can use types like Box, String, Rc, Cell, etc. And then just .clone() all over the place. But your code will run slowly. And thats still way more annoying to use compared to other languages. I don't know any other language which makes you write "Box" to put an object on the heap.
I hear the criticism. It is legitimately really hard to learn how to write rust code in a way that makes the borrow checker happy, especially at first. I'm still avoiding async rust after getting my feet burned by it a couple of years ago. As another commentator has mentioned, rust makes you experience all the pain up front. Its a lot of pain for new programmers to the language - probably too much.
I genuinely believe rust will never achieve the popularity of languages like Python, Go or Javascript. Its just so much harder to learn. I don't think there's anything wrong with that. Most programmers don't need to write systems software. I'd rather rust keeps focuses hard on what its good at: situations where performance and correctness are more important than development speed. We have plenty of good programming languages for quickly prototyping things out. There's nothing wrong with using them when they're the right tools.
I've been teaching myself Rust in the evenings for a couple of weeks now and have found the process maddeningly confusing and slow. Even with the Book, which is fine, I ended up getting into rage induced clone/to_owned/etc keyboard mashing dances with the borrow checker. If it weren't so easy to do this kind of dev with emacs, I might have given up earlier.
On the other hand I occasionally get these moments where my code just "clicks" and it feels good and _seems_ elegant. But, pedagogically speaking, at this point I'm just trying to learn through immersion, not through theory and understanding.
Honestly I'd probably be way done with this little project if I had started in Go, but I think I'm going to stick with Rust for a while.
reply