> 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.
> I'm not that familiar with Rust but, as far as I can see, it makes sense to say that Rust's borrow checker is an interface to a O(1) garbage collector (as fast as no GC at all), that the programmer has to implement at compile-time for all values. All the borrow checker-code is compiled into a no-runtime-overhead garbage collector, that is embedded into the resulting generated code.
Not really : the borrow checker is just a compile-time analysis tool which checks that you respect some conventions with pointers :
- each value has only one owner
- each pointer to a value must leave less time than the target value
- you must only have one mutable pointer to a value
- you can have as many immutable pointer to a value as you want as long as there is no mutable pointer to the same value (think of a statically-checked version of a RW-lock)
The borrow-checker isn't responsible for freeing memory at all. Freeing memory is done by RAII like in C++, but the conventions enforced by the borrow checker ensure that the deallocation of memory is safe.
> just that the borrow checker doesn't fully check here, which for certain core algorithms can be necessary
Why is that? Does this imply that there are certain algorithms you simply can't write (efficiently) when using the borrow-checker? (I don't know Rust btw, but have plans to start learning very soon)
I'm not just talking about the borrow checker though - Rust has a much more strict type system in general. It's really explicit about everything, for example you can't just `println!("{}", some_path)` because `Path` might contain invalid UTF-8 that can't be converted to a string.
Nit: Rust's affine type system is orthogonal to its borrow checker, rather than underlying it (though they're both crucial to Rust's system of memory safety, of course).
>Actually, a lot of the issues that C/C++ programmers have when they are new to Rust seem to be precisely because idiomatic Rust code (which can help to avoid borrow checker issues) is often in a more functional Scheme/OCaml style.
Well, those are not the issues that concern me, or that most (in blogs/HN posts/comments/etc I've read) consider the hard parts. It's the borrow checker.
Rust's borrow checker is not only for multi threading and memory safety. It allows someone designing an API for any kind of data structure to guarantee that the user is using it correctly, by making their code not compile when they are using it incorrectly (basically what any type system provides, but fancier). What you call borrow checker overhead, some would call fixing problems before they occur.
> 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.
> In Rust it's the overt explicitness of the borrow checker, and the friction it adds to one's programming.
Most, and perhaps all of the "friction" that the borrow checker adds to exploratory programming can be fixed with comparative ease by adding a few well-placed `.clone()` calls (for immutable data) and/or changing some type declarations to `Rc<RefCell<…>>` - at a minor cost to performance, in both cases. One might think that Rust needs to do more to support this use case, but given the performance implications, this is one example of how explicit can be far better than implicit. Regardless, it's absolutely the case that you can write "ALGOL" in Rust, just as you can write FORTRAN in any language. The code might not be at peak performance, it might even leak memory in rare cases, but it will do what you expect it to do otherwise.
> Rust borrow checker requires special annotations and restrictions put on the code to do its job.
This is a good thing, because it makes lifetimes and ownership explicit and visible in the code. It serves the similar purpose as type annotations in function signatures.
> Or having multiple mutable pointers/reference to the same object
Sure you can have that with `unsafe`. And this is a good thing, because multiple mutable pointers to the same object is at best bad coding practice that leads to unsafe code, and you should avoid that in any language, including the ones with GC. Working with codebases where there are multiple distant things mutating shared stuff is a terrible experience.
If a C/C++ version of "borrow checker" could mark such (anti)patterns at least as warnings, that would bring a lot of value.
The point of the borrow checker is that it's run at compile time rather than runtime. Putting something similar into the architecture necessarily implies a runtime check, which is similar to the idea of a smart pointer, which exists in Rust as well as other languages.
> 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.
I agree with this. My experience was that the learning curve for Rust has a huge kink in it right where the borrow checker is.
Type checkers provide tools that make it comparatively easier to ignore when they could be wrong or are getting in the way. To do something in a type-safe manner often doesn’t require re-abstracting behavior.
> just borrowed from other languages and put together.
Most of these things are new to the systems space, though. Things like algebraic data types, etc. Also usable affine types.
> lifetimes are still too young to be worth the trouble.
I've heard pretty much the opposite. Firstly, nobody who uses Rust calls the feature "lifetimes" (it's usually called borrowing or borrow checking). That's the name of the syntax, and while the syntax is new and somewhat confusing it doesn't pop up that much thanks to elision.
Borrowing has a learning curve, yes. But really, it's an equivalent set of principles to what you would do in C++ to keep your pointers safe. The difference is that you can write small C++ codebases without worrying about "C++ borrowing", whereas you need to think about these things off the bat in Rust.
I think this and the original post misunderstands what the borrow checker fundamentally is. The only way to "abstract the borrow checker" is to write another language on top of Rust.
> 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.
reply