Hacker Read top | best | new | newcomments | leaders | about | bookmarklet login

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



sort by: page size:

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

I hope Rust never does this, as I think it would kill the best thing about the language. There's almost no point in using Rust if you're not interested in leveraging the borrow checker to write safer code. Just write C++ instead.

The last thing the Rust community needs is to be flooded by new users who don't bring with them a culture of writing safe code. If you give users the ability to just turn off the borrow checker, they'll leave it off and ship code in debug mode.

Many users find the borrow checker perplexing only because they can't understand why the code they write day in and day out in other languages gets rejected by Rust. Well the answer is likely because it's unsafe in some subtle way, and the borrow checker caught it before you even had an inkling there was an issue. Best to get used to writing safer, well-architected systems first, and then you'll have an easier time satisfying the borrow checker.


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


> You still have to fight with borrow checking in Rust as in C++, it’s just not automated.

This is so misleading. Sure you still have to think about borrowing in C++ but in C++ you only have to convince yourself that you got it right. You don't have to convince a dumb child that barely understands variable scopes.

The benefit is of course that the dumb child is 100% reliable so if you can convince him then you are 100% sure you got it right...

But please can we stop pretending that it isn't a massive pain in the arse to convince the borrow checker that your code is ok.

Also as someone else pointed out, if you are only writing single threaded code the mutable borrowing rules are needlessly restrictive.

I like Rust but it really irks me when people pretend it is as easy as C++. It isn't. It is much harder - it's just that when you're done you have (probably) a better result.


> Personally i feel the concern over the borrow checker is way overblown. 95% of the time the borrow checker is very simple.

I have been using Rust professionally as well and had a different experience. For anything singlethreaded I agree with you. For any asynchronous behavior, whether it's threads or async or callbacks, the borrow checker gets very stubborn. As a response, people just Arc-Mutex everything, at which point you might as well have used a GC language and saved yourself the headache.

However, enums with pattern matching and the trait system is still unbeatable and I miss it in other languages.


> It breaks your intuition, and it constantly feels like the compiler is telling you that you’re writing code wrong.

I had the same experience with Rust, and you know what? It was right. My intuition was in fact wrong, and the code the borrow checker complained about would have lead to memory access violations in other languages. Rust catches it at compile time so my program doesn't fall over at runtime. If you think you know better than the compiler, good on you, more power to you. But in my experience I've found that trusting the borrow checker has lead to rock solid stable code that is performant and never crashes during runtime with segfaults and other symptoms of bad code.


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


> If you subscribe to this idea, then it sort of follows that Rust's borrow checker may be "in the wrong place". That is, rather than forcing you to write code that is memory safe in a particular statically verifiable way, Rust could have instead enforced memory safety by injecting run-time checks into the code and optimizing them out when it recognizes code that appeases the borrow checker.

That kind of lack of transparency about what in the hell your code is doing at runtime is really inappropriate in a system's language.

It's an interesting idea, and it would be neat to play with in a language that wanted to restrict itself to more business-logic level safety concerns, but it would absolutely come at the cost of not being appropriate for systems-level tasks.


> > Wanting to ship without violations does not mean you are not interested in getting your algorithm working, first. Often enough, you will abandon your laboriously borrow-safed code without shipping it, because you figured out a better way. But! you had to borrow-safe the new way, too, before you could even try it out.

This is a great idea! It made me realize why I still choose Python for prototyping. (I had been wondering...)

I absolutely love Rust's guarantees and prefer its memory management over GC. But nothing beats no memory management! Make everything Arc, make everything static---I don't care! My program will only live long enough to print "OK" and reach the next TODO, anyway.

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

That's quite okay. "Process end" is as good a time as any, in this case.

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

I don't know why programs with and without overflow and bounds checking are the "same" while programs that close unused sockets at different times are not, but it's okay---we don't have to call it "debug" mode. "Development" or "prototyping" mode both sound fine to me.

> 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 think this is precisely the point. In other languages we can just create objects without worrying about where they should be stored. This is good because most of the objects -w-e- some people create don't get stored anywhere, anyway. They don't make it to the first release.

--

And yes, I realize I'm describing what is, from some angles, a completely different language. But I think one of the original discussion points is "why Rust-like languages keep popping up"? This is my take.

I didn't know where to squeeze this in, but the biggest annoyance in my (quite limited) time with Rust was: refactoring functions. I like breaking up functions into smaller chunks: `do_xy() { ... do_x(); ... do_y(); }` especially where a block of code could really benefit from being under its own name. Developing in Rust, I often found myself looking up internal types for the parameters/return type, and sometimes fighting the borrow checker. (I'm not sure type inference would be a good solution to the first, and I believe the second is mostly due to lexical lifetimes?)


> If you're fighting the borrow checker in Rust, you'd probably have been fighting segfaults and use-after-free in C / C++.

That is in my ( admittedly limited) experience just not true. There's plenty of things that are perfectly safe that the borrow checker just doesn't understand.

The borrow checker can prove that a subset of things is safe. But the borrow checker being unable to prove something doesn't mean it's not safe.


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


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


> Their complaints about borrow checking rules at the interop layer ring hollow to me.

It's actually a big problem. There's a whole lot of Rust library API's that are only provided with an idiomatic "safe" interface, but this actually imposes stronger, more demanding preconditions on those API calls than are warranted by the actual code, which could easily work with e.g. raw (possibly aliased) pointers, or owned-but-pinned (non-movable) data. This creates unneeded pitfalls in Rust-C/C++ interop. The counterargument is that future versions of that library code might benefit from those stronger preconditions, but that's more of a theoretical point, it just doesn't apply in most cases.


> The static analysis of the borrow checker is not able to figure out when you're headed for trouble via this route.

This route exists purely because you can't statically prove some programs correct. Without it, the compiler would have to reject some correct programs and the language would be more limited.

This is unfair to complain about it when the GC-based alternatives push all of memory management to runtime, so the surface for getting panics is a lot larger. Rust's solution is not perfect but still the best you can get.

> Designing the data structures for something tightly coupled with concurrency, like a window manager or a game, is difficult.

It is difficult in any language. The difference is that in Rust you face that difficulty when trying to design/compile your program, and in other more permissive languages you face it when your customer reports a bug. The cost of fixing problems is lowest when they are detected early, so Rust has a clear edge here.


> 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

    LinkedList::Cons(word, words)

> 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 borrow-checker in Rust is kind of silly tool to use in the context of a managed language

I suspect a lot people are drawn to Rust more because of its type system and great tooling. I'd be perfectly happy with a version of Rust that swapped out the borrow checker for a GC, but such a language doesn't exist today, and Rust does exist.


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

Here is a good article that arrives at the opposite conclusion: https://kyren.github.io/2018/09/14/rustconf-talk.html

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.


> I have this intuition that the borrow checker might enforce a way of working that fundamentally stops you from doing things that aren't _really_ compatible with the way a CPU actually works on the lowest level.

No, I think you are reading too much into it. One is free to Box-allocate/pointer-chase on every line in Rust without any trouble. Sure, as other low level languages, rust also prefers/makes stack-allocation more ergonomic/the default, which is a quite cache-friendly way of operation but not even the stack is “native” in and of itself - it is just a frequently and extensively used part of the “heap”.


> "If borrow checker rejects your code, it was wrongly structured anyway"

Nobody is claiming that? At least not in this thread.

I think your confusion stems for thinking that "Rust == borrow checker". The borrow checker is a tiny part of Rust.

> The mental overhead of rust

I find the mental overhead of Rust to be smaller than that of Python, Go, Java, Haskell, C, C++, Lisp, and pretty much any other language that I've ever used.

In Rust I don't have to think about multi-threading, data-races, memory management, resource acquisition and release... the compiler does the thinking for me. I also can refactor and reshuffle code at will, and trust the compiler to catch all errors.

Changing a type that pretty much every translation uses in a 500k LOC code base and add multi-threading to it? No problem, just try it out, and if it compiles, it is correct.

I can't say the same of Go, Python, Java, etc. where threading errors are impossible to debug. And well, it wouldn't even be worth trying. It would be impossible to make sure that the code is correct, even after fuzzing for weeks.

next

Legal | privacy