2. I agree on this one. The basic solution is "well instead of a pointer use a index into a vector" which has some correctness advantages (but less than "normal" rust does), but generally a bit of extra programmer pain.
3. See 2.
4. The compiler takes good enough care of you during things like this that it's basically painless.
#4 highly depends on a project size. On big projects, some of these decisions might become effectively one way doors.
I've been struggling a bit with these issues on the project of ~70k lines. I cannot even imagine what the refactoring would look like if we had, let's say, 1 million LOC.
To be fair, though, we use Rust the way it wasn't specifically designed for (large "enterprise" software, think Java-like enterprise).
I think, potentially, Rust could offer a much better story for this kind of software (assuming we are not mad and the issues we are facing are not because we doing something completely wrong :) ). In my opinion, the key thing would be to allow building "bridges" between pieces of the system which are "ownership-incompatible", so your decisions around ownership are not "one way doors" anymore (at the cost of translation / adapter layer).
Some random things which I think would be helpful:
1. Better self-referential structs, to allow going from "owned A + borrowed B" into "fully owned A+B" (rental crate helps here, though). Basically, hiding lifetimes in scenarios where you cannot easily change the original data structure to "own".
2. GATs. Honestly, this one is my speculation, but it seems like certain patterns which are hard to express now (abstraction of a "mutable reference", for example) would be possible with GATs. In our case, this would allow to bridge the gap between "trait object" world and "parametric over trait" world. The issue I was having is it is hard to express "mutably borrow from self" with traits (this is something similar to the issue "streaming iterator" crates solves). I was able to hack something using arbitrary self types, but it's quite... hacky.
3. Trait objects stable(r) ABI. Again, purely my speculation, but would allow to go back from "trait reference" world into "trait object" world. I won't go into details here, but trait objects want to "borrow" from something and it is not always easily possible (think that favorite vector+indices data structure) -- being able to "fake" those borrows would be nice (maybe).
Issues #2 and #3 specifically happens around deciding on data structure: regular structs have one set of tradeoffs, vectors with indexes -- another. In a big enterprise software, I would like to have an option to use whatever works in a particular spot and still have it API-compatible to the rest of the system.
> #4 highly depends on a project size. On big projects, some of these decisions might become effectively one way doors.
The transformations between T, Box<T>, Rc<T>, Arc<T>, etc. are mechanical, so I expect someone will write a refactoring tool that makes a giant PR for you automatically. (Subject to certain limits, like if you're actually cloning the ref-counted pointer, it's indeed harder to go back.) Would that satisfy your need?
> To be fair, though, we use Rust the way it wasn't specifically designed for (large "enterprise" software, think Java-like enterprise).
IMHO, this is a valid use case for Rust. I'm not saying everyone should stop using Java (in some cases I think it's significantly faster to write) but Rust has some strong performance advantages and no data races in safe code.
It's not always possible to change the data structure -- different ways of modeling data have different trade-offs. So, for me it is about not having to make a choice than about tools that will help you to change your mind.
Also, it could be something like structure coming from a 3rd-party crate using borrowing and you want to stick it into "Arc" of some sort. Or put it (with the thing it borrows from) into a lifetime-less struct, so you don't have to care about these lifetimes.
> It's not always possible to change the data structure -- different ways of modeling data have different trade-offs.
I agree with "different ways of modeling data have different trade-offs", but I don't understand how that leads to "it's not always possible to change the data structure". I revisit trade-offs all the time.
Could you explain? I might need a concrete example.
> Also, it could be something like structure coming from a 3rd-party crate using borrowing and you want to stick it into "Arc" of some sort. Or put it (with the thing it borrows from) into a lifetime-less struct, so you don't have to care about these lifetimes.
Yeah, certainly the refactoring becomes harder (maybe implausible to do automatically) when you can't change both sides in one PR, and when you have to convince someone else to change their interface / bump the major version. It still can be done (partially?) by hand at least; it's just a matter of cost/benefit.
>Could you explain? I might need a concrete example.
You might want different trade-offs in different places.
Like, in our case, the conflict is between three different representations:
1. Typed Rust structs
2. Vector with indexes
3. Untyped structs (HashMap of strings to values, essentially)
None of them covers 100% of the use-cases we have (though we also not sure exactly are these the use-cases we will have year from now? three years from now?), and some parts of the system needs to work with all of them.
>Yeah, certainly the refactoring becomes harder (maybe implausible to do automatically) when you can't change both sides in one PR, and when you have to convince someone else to change their interface / bump the major version. It still can be done (partially?) by hand at least; it's just a matter of cost/benefit.
One case was Transaction from postgres crate, which uses lifetime. But I want to stuff it in Arc. Would be possible, if Transaction itself used Arc instead of borrowing, but there are about zero reasons for them to change API that way.
Cloning is not always possible (performance reasons, non-cloneable data, etc) and would not necessarily remove lifetime (for example, it could be a struct, defined somewhere else, with a lifetime parameter).
2. I agree on this one. The basic solution is "well instead of a pointer use a index into a vector" which has some correctness advantages (but less than "normal" rust does), but generally a bit of extra programmer pain.
3. See 2.
4. The compiler takes good enough care of you during things like this that it's basically painless.