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

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



sort by: page size:

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


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


> To be honest, everything in Rust looks a bit ugly to me.

I write a lot of Rust code at work, and I admit that it can sometimes be pretty noisy. There are several major contributors to this:

1. Rust offers fine-grained control over pass-by-value, pass-by-reference, and pass-by-mutable reference. This is great for performance. But it also adds a lot of "&" and "&mut" and "x.to_owned()" clutter everywhere.

2. Rust provides support for generics (aka parameterized types). Once again, this is great for performance, and it also allows better compile-time error detection. But again, you wind up adding a lot of "<T>" and "where T:" clutter everywhere.

3. Usually, Rust can automatically infer lifetimes. But every once in a while, you want to do something messy, and you end up needing to write out the lifetimes manually. This is when you end up seeing weird things like "'a". But in my experience, this is pretty rare unless I'm doing something hairy. And if I'm doing something hairy, I'm just as happy to have more explicit documentation in the source code.

Really, the underlying problem here is that (a) Rust fills the same high-control, high-performance niche as C++, but (b) Rust prefers explicit control where C++ sometimes offers magic, invisible conversions. (Yes, I declare all my C++ constructors "explicit" and avoid conversion operators.)

Syntax is a hard problem, and I've struggled to get syntax right for even tiny languages. But syntax for languages with low-level control is an even harder problem. At some point, you just need to make a decision and get used to it.

In practice, I really enjoy writing Rust. It's definitely not as simple as Ruby, Python or Go. But it fills a very different ecological niche, with finer-grained control over memory representations, and support for generics.


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


> The borrow checker in Rust might be to important to turn off even sometimes.

Variations of this point come up often, that the main reason rust compile time is slow is typechecking or borrowchecking, but that’s not true, while those operations are expensive, it’s monomorphization (and llvm) that are the main drivers of compile time currently. Building a compiler on top of llvm is unfortunately going to be sluggish for feedback loops , regardless of your type system.

Also, unlike LTO, borrow checking is not just an optimization, turning it off would be like allowing you to add arrays and booleans together: meaningless and wrong. Anyways, you wouldn’t save more than ~10% time.

Apart from the rant above, I do agree that having the ability to tune optimization levels for development vs prod is good, I would like it if it were easier to do stuff like PGO or other stupidly expensive optimizations for releases.


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


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


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


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


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

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


> - The type system is unusual, and complex. It's hard to do anything without templates.

You need generics to have the borrow checker that you praise above. Otherwise references would be basically crippled.

> The template libraries are heavily biased towards closure-oriented functional programming.

No, they aren't. I use for loops all the time in Rust, and it's totally natural to do so. My general guideline in my own code (which is followed in projects like WebRender and Servo layout) is that if it's one line, I use the functional idiom; if it's more than one line, I use a for loop.

> Parts of expressions are nameless and have no visible type. This is terse but hard to maintain.

Are you thinking of unboxed closures here? If so, they're necessary to avoid unnecessary heap allocations in normal code. If we didn't have that, then we couldn't compete with C++11 closures.

> - The hacks needed to avoid the need for exceptions are uglier than exceptions.

Unwinding is the single most divisive issue in Rust. We would alienate most of our community (I'm not kidding) if libraries required it. For a time, there was a language fork over the issue until it was resolved.

> Unlike the borrow checker, none of these things are clearly improvements. They're just different.

No, they're needed for the borrow checker and most of the other features to work.

> I would have gone for Python-type exceptions rather than error enums and macros

Then, as above, you instantly alienate all embedded developers.

> single-inheritance OOP rather than traits

This doesn't work in practice unless you introduce interfaces: it's way too inexpressive. This is why Java and C# have interfaces. Traits are just interfaces.

> Python-type with clauses rather than RAII.

You want to force an extra layer of indentation for every Box or Vec you create, and leak memory if you forget to write "with"? No thank you!


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

Fwiw, my shop uses Rust in some basic web applications and lifetimes are not used in day to day programming. It heavily depends on what you're doing. Are there some applications/libraries/etc where we're using lifetimes? Definitely. But for us they're very niche. AND, perhaps most importantly, even in those you _still_ don't have to. If you're okay with the performance trade off, which we most certainly are (we have some Python usage for example), adding in basic reference counting can be a no-brainer tradeoff.

I actually quite like lifetimes. I have some side projects where i use them extensively. But just like standard Generics they're a tool you can use, but that doesn't mean you always should. Just like accepting a `s: &str` instead of `s: S) where S: AsRef<str>` can make your life cleaner and simpler, using `Rc<Foo>` can equally simplify.

Just my 2c :)

[1]: because we like it, no other reason.. really, a team of ~30.


> Rust has a lot of things that C doesn't, like classes, lifetimes, and the borrow checker, which can be jarring to deal with for programmers used to other languages.

If you are a competent C programmer - the kind that "doesn't make" memory errors, you have to be manually keeping track of lifetimes and borrows.

Rust takes that mental effort and offloads it to the compiler.

Lifetimes and the borrow checker will likely be jarring for people coming from languages where you don't need to worry about memory allocation and ownership, but if you are coming from C or C++, you will likely find the borrow checker a relief (I did).

> the compiler won't let you do the "bad" things, so you have to get everything write from the word go.

And it's wonderful! Finding and fixing problems at compile time is the fastest and cheapest way to do it, and is also a productive way to refine your abilities.


> I'd hoped that language features like the typestate stuff that used to be in Rust would someday make the work required to use sound analysis tools in production code smaller. I'm not sure if much thought has been given to what kinds of accommodations languages could give to ease static analysis while still being programmer friendly.

Well, since the Rust borrow check is basically a static analysis that's always on and is required to pass for the compiler to emit code, we've put a lot of thought into how to make it as programmer friendly as possible. The final trick that seemed to got it to fall into place was restricting data to only one mutable reference at all times—this was a restriction that's easy enough to understand and can be readily explained through error messages and tutorials. There's still a learning curve, of course, but I think we've got the system down to a reasonable level.


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

It's not that easy. The borrow checker gives you memory safety and thread-safety. In C++, you always have to worry about memory safety, so here the ownership model and borrow checker is a pure win. However, in C++ you don't have to worry about thread safety in a single thread...there's no barrier to passing pointers around and mutating things from different places in your code. In rust, the borrowchecker disallows shared mutable state...that's a huge win if you want to parallelize your code later on, but it also means that things that are trivial in C++ take a lot of thought and effort in rust, especially if you're thinking in a C++ paradigm (as opposed to coming from a functional language).


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


> * The borrow checker. Memory safety is great but there are plenty of memory-safe designs that the borrow checker complains about. With modern C++ and sound design techniques, memory safety is not something I worry about much in my C++ projects, and the static analyzer proves me out on that. In particular, a typical application design involves some variation of the Observer pattern (implementations can vary wildly),

Don't you still have Iterator invalidation to keep track of and I thought I saw posts regretting using `std::string_view` because it is easy to reference deleted memory.

Anecdote: I maintain a template language library for Rust. I saw the potential for it to speed up if I used more borrowed data (like `&str`, Rust's version of `std::string_view`). I gave it a try and once it compiled, it just worked without crashes. I reflected back on if this was in C++ and the conclusion I came to was that maintainable code is a much higher priority than performance and that any future change in a similar C++ code base would require global analysis to make sure it was safe, making the performance gains not worth the lack of maintainability. In Rust, its been trivial.

> Where are the functors??!?!? Lambdas are great and all, but I can’t create unbound trait/struct functors and then invoke them on strict references later? This is a huge limitation in runtime flexibility for application development. Boost::function and boost::bind worked with VC6 twenty years ago! When I realized Rust didn’t have functors, I became much less interested in the language.

The fact that you can't `impl Fn` bothered me for a long time. It was one of the many examples of where Rust felt unfinished.

Recently, I've seen an inverted pattern for this, define a trait and `impl MyTrait for Fn`. I've found I much prefer this pattern over the cases where I would have used a functor. Granted, there are more ad-hoc cases where functors would be better.

> No function overloading. This is just silly and onerous. Rust could create sensible restrictions to avoid ambiguities and C++’s ADL/type coercion complexity but there’s nothing ambiguous about having two functions with different arity sharing the same name. The hardest problem in CS is naming things, and Rust’s lack of function overloading makes it harder still.

I thought I'd miss this but it hasn't been as bad as I expected.

next

Legal | privacy