It’s a common fallacy that the borrow checker is just a substitute for a GC, when it’s much more than that: it can give you safety guarantees (absence of mutable aliasing and data races) that e.g. Java or Go cannot.
This is true, but at least for me these additional guarantees aren’t preventing many bugs. In particular, my code is rarely subject to data races–usually any shared state is a file or network resource that is accessible by multiple processes on multiple hosts and Rust’s borrow checker doesn’t help in these regards, but I still have to pay for it with respect to productivity (yes, I know that productivity improves with experience, but returns diminish quickly and the apparent consensus seems to be that a wide productivity gap remains between GC and BC).
I’m not taking shots at Rust; I’m glad it exists, it’s impressive and ambitious and etc. It just isn’t going to eat Go’s lunch unless there’s some (ergonomic) way to opt out of borrow checking.
Because for a language with unrestricted semantics around mutability, like Go, this would be equivalent to the halting problem.
Languages like Rust or similar use linear/affine/unique types to restrict semantics in a way to make such type of analysis possible. That’s the whole point of linear types.
You can do it at runtime though (see Go’s race detector), and while that’s immensely useful, it’s not the same thing.
They can, to a small extent. C compilers too, or Python or whatever. But if you don’t have Rust’s move semantics and Rust’s borrow system, then the compiler doesn’t have a lot of information to do the analysis with.
Compiler has to work in concert with the language – Rust isn’t just C++ with more rules on top, lifetimes and aliasing are a visible part of type system the programmer interacts with. The user spends more time explaining in more details what’s going on to the compiler: in Java you have T, in Rust it’s your task to pick between T, &'a T or &'a mut T. In exchange for this extra annotation burden, compiler can reason precisely about aliasing and rule out things like data races or iterator invalidation.
As an analogy, we can type-check JavaScript to some extent, but we really need TypeScript’s extra type annotations to make this really work.
In other words: Rust is referentially transparent. If you would like a referentially transparent language—or a language which allows you to control and manage mutation—without spurious copies I suggest haskell or ocaml.
Yeah, they don’t attempt to guarantee thread safety:
Rust permits only one mutable reference to a value at a time. For greater flexibility, (standard) Rust supports interior mutation of an object through an immutable reference to it [19]. The programmer may borrow a special reference to the value that permits mutation, and the runtime ensures that only one such reference can exist at a time, enabling a safe relaxation of compile-time checks. With Bronze, mutation is permitted through all references to each garbage-collected object, with no extra effort. A key tradeoff is that Bronze does not guarantee thread safety; as in other garbage-collected languages, it is the programmer’s responsibility to ensure safety. For example, Figure 1 shows how GcRef simplifies code when there are multiple mutable aliases.
Yeah, Manish Goregaokar raised this and a couple of related issues it on Twitter: Bronze doesn’t provide its own sound way to have multiple mutable references (which can be a problem in single-threaded contexts too, e.g. with iterator invalidation), and he thought that for the particular exercise they gave students here, RC might have worked fine and GC might not have been why students found the exercise easier to complete. (I haven’t read the paper, just figured the reference might be useful to folks.)
It sounds like this paper says more about the usability of current interior mutability APIs in Rust than about GCs. I can attest to the fact that it’s very hard to pick up and use the interior mutability APIs in Rust. I’d be interested in seeing more work on how those APIs could be changed to improve their usability.
The fact that more Bronze participants withdrew from the experiment makes the numbers hard to compare. But kudos for admitting it!
Others here point out that they sacrifice thread safety for their Bronze approach. The non-GC code could be simplified similarly with some lines of unsafe code (which is a questionable approach, of course).
I know someone who knows one of the investigators. They described the experiment as “How to torture 633 undergraduates for science”. Apparently it wasn’t much fun to participate in..
It’s a common fallacy that the borrow checker is just a substitute for a GC, when it’s much more than that: it can give you safety guarantees (absence of mutable aliasing and data races) that e.g. Java or Go cannot.
This is true, but at least for me these additional guarantees aren’t preventing many bugs. In particular, my code is rarely subject to data races–usually any shared state is a file or network resource that is accessible by multiple processes on multiple hosts and Rust’s borrow checker doesn’t help in these regards, but I still have to pay for it with respect to productivity (yes, I know that productivity improves with experience, but returns diminish quickly and the apparent consensus seems to be that a wide productivity gap remains between GC and BC).
I’m not taking shots at Rust; I’m glad it exists, it’s impressive and ambitious and etc. It just isn’t going to eat Go’s lunch unless there’s some (ergonomic) way to opt out of borrow checking.
why do you say Go (or Java) compilers can not effect such analyses?
Because for a language with unrestricted semantics around mutability, like Go, this would be equivalent to the halting problem.
Languages like Rust or similar use linear/affine/unique types to restrict semantics in a way to make such type of analysis possible. That’s the whole point of linear types.
You can do it at runtime though (see Go’s race detector), and while that’s immensely useful, it’s not the same thing.
Can you show me one that does (i.e. statically detects all mutable aliasing or data races)?
That didn’t answer the question asked.
They can, to a small extent. C compilers too, or Python or whatever. But if you don’t have Rust’s move semantics and Rust’s borrow system, then the compiler doesn’t have a lot of information to do the analysis with.
Compiler has to work in concert with the language – Rust isn’t just C++ with more rules on top, lifetimes and aliasing are a visible part of type system the programmer interacts with. The user spends more time explaining in more details what’s going on to the compiler: in Java you have
T
, in Rust it’s your task to pick betweenT
,&'a T
or&'a mut T
. In exchange for this extra annotation burden, compiler can reason precisely about aliasing and rule out things like data races or iterator invalidation.As an analogy, we can type-check JavaScript to some extent, but we really need TypeScript’s extra type annotations to make this really work.
In other words: Rust is referentially transparent. If you would like a referentially transparent language—or a language which allows you to control and manage mutation—without spurious copies I suggest haskell or ocaml.
How does it obtain safety while allowing multiple mutable references? Is it by enforcing single threaded only code?
Yeah, they don’t attempt to guarantee thread safety:
Bad idea. They should just make the references !Send and !Sync and only use it in single threaded contexts.
That would work, but multiple mutable references are still unsound in a single-threaded context. See: https://manishearth.github.io/blog/2015/05/17/the-problem-with-shared-mutability/
True. They would also need to add a runtime check when getting a mutable reference, like RefCell.
Yeah, Manish Goregaokar raised this and a couple of related issues it on Twitter: Bronze doesn’t provide its own sound way to have multiple mutable references (which can be a problem in single-threaded contexts too, e.g. with iterator invalidation), and he thought that for the particular exercise they gave students here, RC might have worked fine and GC might not have been why students found the exercise easier to complete. (I haven’t read the paper, just figured the reference might be useful to folks.)
It sounds like this paper says more about the usability of current interior mutability APIs in Rust than about GCs. I can attest to the fact that it’s very hard to pick up and use the interior mutability APIs in Rust. I’d be interested in seeing more work on how those APIs could be changed to improve their usability.
Whatever the merits of this particular comparison I’m excited to see language usability research being done.
The fact that more Bronze participants withdrew from the experiment makes the numbers hard to compare. But kudos for admitting it!
Others here point out that they sacrifice thread safety for their Bronze approach. The non-GC code could be simplified similarly with some lines of unsafe code (which is a questionable approach, of course).
I know someone who knows one of the investigators. They described the experiment as “How to torture 633 undergraduates for science”. Apparently it wasn’t much fun to participate in..
Hm. I’m an undergrad, and it sounds like it would’ve been fun to participate in! (n=1)
Do you know why?
Probably just ‘cause most undergrads don’t need another 12 hours of work added to their life.