> 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.
This is where I'm at right now. When the borrow checker yells at me, I find I can generally fix it with a .clone() or a .to_string(). I'm new enough with the borrow checker that I'm not quite sure when such cloning is necessary and when I'm unnecessarily allocating more memory -- or more importantly, what I should be doing instead when it's not necessary -- but I hope that as I write more code, I'll get better at it :)
A bad habit that I've fallen into is to needlessly allocate in order to avoid having to think about lifetimes. As in, better change that struct to make that str a String -- and then not have to worry about lifetimes at all. That's just laziness, though...
> since it's more difficult to reason about strings as they're heap allocated
I don't think them being heap allocated is the issue. I think the issue you're running into is that String doesn't implement the Copy trait, meaning they're not automatically copied for you. Which is what you want, because you don't want to be implicitly copying large strings without realizing it. But yes, it does mean you have to learn the borrow checker.
> FWIW, as a JS/Python programmer I didn't run into borrow checker issues on my most recent Rust foray -- I think it's gotten easier over time with stuff like non-lexical lifetimes?
It also varies hugely according to what kind of program you're writing. Some programs naturally have borrowcheck-friendly structures, others don't.
For example, i wrote a program which processes network packets. There is a main loop which reads packets, then passes them down a pipeline of filtering, extraction, and other processing, eventually sending out more network packets. From the compiler's point of view, the pipeline is just a series of nested function calls. Each call can safely borrow from any of the enclosing scopes. Some of the stages are stateful, but they manage their own state, copying data to and from the packets. There's nowhere in a program like this that you run into a problem with borrowing, it all just flows naturally.
> You'd be constantly fighting the type checker for memory issues.
I think you mean borrow checker for ownership issues...
> why doesn't Haskell take the same route as Rust and ditch its garbage collector?
The Rust language was designed in a way (with ownership) that doesn't need a GC, while Haskell have many different properties (such as lazy evaluate everything) that I don't think would work well without a GC.
The borrow checker is usually a nuisance when you're writing highly mutable code, which isn't a problem for functional code. If you need to store the same data in multiple structures, the sibling is right, .clone() and .to_owned() will usually fix your problem with a very small overhead.
I struggled mightily with the borrow checker and lifetimes when I first got started until I got some good advice to just use the heap. String, Vec, .clone(), etc. Just write the code you want and don't worry about allocations. You can fix that all up later when you're more comfortable. It was kind of a cognitive game-changer for me. And now I love Rust and I've gotten better at it.
> Also btw "borrow checker fighting" is not much of an issue for experienced Rust developers. You quickly get used to how to write code to avoid borrow checker issues. And in general, writing code that way tends to be a good thing anyway.
> It is true that this needs to be done with some care otherwise you can end up infecting your whole codebase with the “workaround”
It's a "workaround" precisely because the language does not support it. My statement is correct - you cannot turn the borrow-checker off, and you pay a significant productivity penalty for no benefit. "Rc" can't detect cycles. ".clone()" doesn't work for complex data structures.
To be honest I just clone and unwrap everywhere when writing the first iteration. If I care more, then I'll refactor to remove unnecessary clones. I really haven't felt the pain of the borrow checker like some people say.
I think everyone agrees that "fighting" the borrow checker is annoying, however at the risk of sounding like a Rust apologist I find that a couple interesting things have happened over ~2 years with the language:
1. The borrow checker usually kicks in when I want to take shortcuts that would bite me in the future. 90% of issues I hit with it can be solved with copy/clone.
2. When I take the time to satisfy the borrow checker I find my architecture is better. Usually I'm tripping the borrow checker because I don't have a clear single-owner or separation of concerns.
Also, if you're looking for an "easy" to program languages there's tons of them out there and it definitely doesn't make sense in my mind to use Rust where C#/Erlang/Java/etc fit the domain space better.
This isn't going to be appealing to Rust current users though, because “fighting the borrow checker” is a learning curve issue, you don't fight the borrow checker anymore once you've internalized its rules.
> or making gratuitous copies of data to satisfy the borrow checker.
You get it backward. In Rust there's less gratuitous copies, not more, because the ownership rules and the borrow checker gives you compile-time guarantees, while “defensive copies” are common in C++ for instance, “to be safe”.
> The borrow checker works on "struct granularity", but it would be much more flexible and convenient if borrowing would work on memory location granularity (for instance passing a struct reference into a function "taints" the entire struct as borrowed, even if that function only accesses a single item in the borrowed struct - this 'coarse borrowing' restriction then may lead to all sorts of workarounds to appease the compiler, from 'restructuring' your structs into smaller pieces (which then however may fit one borrowing situation, but not another), or using 'semantic crutches' like Rc, Cell or Box.
That's valid, thanks for pointing it out. I seem to recall the team lately mentioning they are starting to consider fixing that. And yes that's a real productivity killer, happened to me as well in the past.
Stop caring about writing .clone() until you feel comfortable enough to care.
The biggest thing I see people go crazy with in Rust is trying to avoid cloning things. I've watched it block people from progressing in the language. Yes, you ultimately don't want to clone everything everywhere, but getting something built in Rust and then iterating backwards is often a better way to learn the language than trying to be "correct" from the start.
Cloning often removes any need to deal with lifetime signatures or what-have-you. If you're bothered by a full clone, grab Rc/Arc and use that trade-off in the beginning - it's not going to kill you. These changes make the syntax of the language fairly comparable to other languages.
tl;dr: .clone() things, measure if it's a problem, and then work backwards to the state you feel that the language/borrow-checker/whatever is pushing you towards.
> getting used to the borrow checker's rules is a big part of Rust's learning curve.
It’s also a big part of C and C++’s learning curves, it’s just that the compiler doesn’t tell you about it; you’re being taught by segfaults and silent memory corruption instead.
> What exactly do you mean? How does it differ from any other language's type system?
Part of rust's type system is sub-structural: by default, Rust's types are affine, which means you can only use them once (at most, not exactly).
Now this is not super convenient to use and could have efficiency issues (e.g. any time you want to check a value in a structure you'd have to return the structure so the caller can get it back), so to complement this you can borrow (create a reference). The borrow checker is the bit which checks that borrows satisfy a bunch of safety rules, mostly that a borrow can't outlive its source, and that you can have a single mutable borrow or any number of immutable borrows concurrently.
The borrow checker provides for memory-safe pointers to or into a structure you're not the owner of, with no runtime cost.
> [the borrow checker] makes Rust relatively hard to learn and use.
I would say the borrow checker makes rust harder to learn, but it does not make it harder to use once you actually grok it. When that time comes, the borrow checker fades into the background. When I got to that point with Rust, it became much easier to use that other languages for me.
I still fought the borrow checker after a year of using Rust.
And I found many situations while using Rust where I either needed to clone, 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.
> every time the borrow checker throws a fit, it very likely saved you from a memory safety bug.
This is not even close to being true, the borrow checker does not accept all memory safe programs. It tries to do a good job of accepting programs that are both safe and have simple and elegant guarantees of safety across module boundaries (which is good for extensibility and evolvability).
But there are designs that can only be expressed in Rust by using constructs that replace some of these compile-time guarantees with runtime constraints.
This is where I'm at right now. When the borrow checker yells at me, I find I can generally fix it with a .clone() or a .to_string(). I'm new enough with the borrow checker that I'm not quite sure when such cloning is necessary and when I'm unnecessarily allocating more memory -- or more importantly, what I should be doing instead when it's not necessary -- but I hope that as I write more code, I'll get better at it :)
A bad habit that I've fallen into is to needlessly allocate in order to avoid having to think about lifetimes. As in, better change that struct to make that str a String -- and then not have to worry about lifetimes at all. That's just laziness, though...
reply