The author may have a point in the idea of borrowing record fields separately. It is possible if we assume that the fields are completely orthogonal and can be mutated independently without representing an incorrect state. It would be a good option to have.
But a doubly-linked list (or graph) just can't be safely represented in the existing reference semantics. Dropping a node would lead to a dangling pointer, or several. An RDBMS can handle that ("on delete set null"), because it requires a specific way to declare all such links, so that they can be updated (aka "foreign keys"). A program usually does not (Rc / Arc or shared_ptr provide a comparable capability though).
Of course a bidirectional link is a special case, because it always provides a backlink to the object that would have a dangling pointer. The problem is that the borrow checker does not know that these two pointers belonging to different structs are a pair. I wish Rust had direct support for such things, then, when one end of the bidirectional link dies, the borrow checker would unset the pointer on the reciprocal end. Linking the objects together would also be a special operation that atomically sets both pointers.
In a more general case, it would be interesting to have a way to declare some invariants over fields of a struct. A mutual pair of pointers would be one case, allowing / forbidding to borrow two fields at once would be another. But we're far from that.
Special-casing some kinds of pointers is not unheard of; almost every GC-based language offers weak references (and Java, also Soft and Phantom references). I don't see why Rust could not get a BidirectionalRef of sorts.
Until then, Arc or array with indexes seem to be the only guaranteed memory-safe approaches.
Also, in the whole article I could not find a single reason why the author chose Rust, but I suppose it's because of its memory efficiency, considering the idea of keeping large graphs in RAM. Strictly speaking, Go could be about as efficient, but it has other inflammation points. C++... well, I hope Rust is not painful enough to resort to that.
It uses unsafe, but few people understand what it really is. Rust has clear invariants (for example, that a pointer to T always points to an existing and valid T) that are enforced by the compiler. Using unsafe code is shifting the enforcement of these invariants from the compiler to the programmer. The advantage compared to C is that unsafe code is limited to undefined blocks that are easy to test.
The data structure you describe should be done exactly this way. And it is not a task for the average programmer.
Many tools deal with sequencing data, which means collections of strings that cannot be parsed or tokenized in any meaningful way. The collections are often very large, and the strings can also be very long. The standard algorithmic toolkit you learn when doing a CS degree (or a PhD in algorithms) is inadequate with such data. Hence, if you go to a CS conference focused on combinatorial algorithms and data structures, the presented work is often motivated by applications in bioinformatics.
If a language is bad, but you must use it, then yes learn it. But, if the borrowchecker is a source of pain in Rust, why not andmit it needs work instead of saying that “it makes you better”?
I’m not going to start writing brainfuck because it makes me a better programmer.
The point is the borrow checker has already gone beyond the point where the benefits outweigh those annoyances.
It's like... Static typing. Obviously there are cases where you're like "I know the types are correct! Get out of my way compiler!" but static types are still vastly superior because of all the benefits they convey in spite of those occasional times when they get in the way.
I don't think a smarter borrow checker could solve most of the issues the author raises. The author wants borrow checking to be an interprocedural analysis, but it isn't one by design. Everything the borrow checker knows about a function is in its signature.
Allowing the borrow checker to peek inside of the body of local methods for the purposes of identifying partial borrows would fundamentally break the locality of the borrow checker, but I think that as long as that analysis is only extended to methods on the local trait impl, it could be done without too much fanfare. These two things would be relaxations of the borrow checker rules, making it smarter, if you will.
This is a moot statement. Here is a thought experiment that demonstrates the pointlessness of languages like Rust in terms of correctness.
Lets say your goal is ultimate correctness - i.e for any possible input/inital state, the program produces a known and deterministic output.
You can chose 1 of 2 languages to write your program in:
First is standard C
Second is an absolutely strict programming language, that incorporates not only memory membership Rust style, but every single object must have a well defined type that determines not only the set of values that the object can have, but the operations on that object, which produce other well defined types. Basically, the idea is that if your program compiles, its by definition correct.
The issue is, the time it takes to develop the program to be absolutely correct is about the same. In the first case with C, you would write your program with carefully designed memory allocation (something like mempool that allocates at the start), you would design unit tests, you would run valgrind, and so on.
In the second case, you would spend a lot more time carefully designing types and operations, leading to a lot of churn of code-compile-fix error-repeat, taking you way longer to develop the program.
You could argue that the programmer is somewhat incompetent (for example, forgets to run valgrind), so the second language will have a higher change of being absolutely correct. However the argument still holds - in the second language, a slightly incompetent programmer can be lazy and define wide ranging types (similar to `any` in languages like typescript), leading to technical correctness, but logic bugs.
So in the end, it really doesn't matter which language you chose if you want ultimate correctness, because its all up to the programmer. However, if your goal is rapid prototyping, and you can guarantee that your input is constrained to a certain range, and even though out of range program will lead to a memory bug or failure of some sort, programming in something like C is going to be more efficient, whereas the second language will force you write a lot more code for basic things.
Working at a company with lots of systems written by former employees running in production… the advantages of Rust become starkly obvious. If it’s C++, I walk on eggshells. I have to become a Jedi master of the codebase before I can make any meaningful change, lest I become responsible for some disaster. If it’s Rust, I can just do stuff and I’ve never broken anything. Unit tests of business logic are all the QA I need. Other than that, if it compiles it works.
This is not true. Case and point- Java. Many times simpler than Rust, and large codebases are as horrible as C++ ones.
1. NullPointerException. I get some object with a bunch of fields and I don’t know which of them are null. In Rust I am given a struct, and usually the fields aren’t an Option unless they need to be.
2. Complicated design patterns with inheritance. Maybe it’s more of a problem with the culture/ecosystem than Java the language. But Rust doesn’t have inheritance, and traits are less complicated. So you rarely get the same smells.
Compared to C++, Java is easier to debug, but I still have a lot of “wtf” moments trying to understand what the author was thinking. It is almost like the language is so simple to write, people are making the program more complicated on purpose just to make it interesting.
For example, you can have multiple people working on a code base for the second case, and some sub team has a new requirement for added functionality. Now they have to go refactor a whole bunch of the codebase to make all the types coherent. And consequently, shortcuts happen here, which leads to shit codebases.
It really does matter which language you choose if you want correct code.
> programming in something like C is going to be more efficient, whereas the second language will force you write a lot more code for basic things.
Like how string manipulation is so much simpler and easier in C compared to Rust? Hmm.
Also the argument of forcing a language onto a project based on the lowest common denominator of programmers never plays out - this is how you get insanely messy Java codebases. Language choice will never solve poor programming style.
>Like how string manipulation is so much simpler and easier in C compared to Rust? Hmm.
Plenty of libraries for C for this.
That is absolutely untrue. Most Rust codebases have very few uses of `unsafe`. I have written tens of thousands of lines of Rust and used `unsafe` I think once. Maybe twice.
> Language choice will never solve poor programming style.
It absolutely does. Rust pushes you very strongly into the "pit of success". You can Google that term if you want to learn.
> Plenty of libraries for C for this.
I have yet to see a single C program in the wild use anything other than libc for string manipulation. I'm sure they exist but they are the 0.1%.
That is Rust? This is how his system of types and traits works.
This is a pretty important qualification. Most low-level systems code doesn't and can't have this ownership structure. It provides a reason why Rust has more traction replacing code that could have been written in Java rather than e.g. C++ in the domains where C++ excels (like database engines).
I don't think that was ever the intent behind the borrow checker but it is definitely an outcome.
So yes, the borrow checker makes some code more awkward than it would be in GC languages, but the benefits are easily worth it and they stretch far beyond memory safety.