Hacker News new | ask | show | jobs
by gary17the 836 days ago
If you do not want to mess with Rust borrow checker, you do not really need a garbage collector: you can rely on Rust reference counting. Use 1.) Rust reference-counted smart pointers[1] for shareable immutable references and 2.) Rust internal mutability[2] for non-shareable mutable references checked at runtime instead of compile time. Effectively, you will be writing kind of verbose Golang with Rust's expressiveness.

[1] https://doc.rust-lang.org/book/ch15-04-rc.html

[2] https://doc.rust-lang.org/book/ch15-05-interior-mutability.h...

3 comments

A language has a paved road, and when you go off of that road you are key with extreme annoyance and friction every step of the way.

You’re telling people to just ignore the paved road of Rust, which is bad advice.

No, not really. Firstly, there is no significant "friction" to using Rust smart pointers and internal mutability primitives, as those constructs have been added to Rust for a reason: to solve certain borrow checker edge cases (e.g., multiply interconnected data structures), so they are treated by the Rust ecosystem as first-class citizens. Secondly, those constructs make a pretty good educational tool. By the time people get to know Rust well enough to use those constructs, they will inevitably realize that mastering the Rust borrow checker is just one book chapter away to go through out of passion or boredom.
I find quite a lot of friction in being demanded to understand all of the methods, what they do, when you’d use them, why you’d choose one over another that does a slightly different thing, but maybe still fits.

The method documentation alone in reference counting is more pages than some entire programming languages. That’s beside the necessary knowledge for using it.

I don't think it's necessary to understand every single `Rc<T>` method[1] to use Rust smart pointers to learn Rust. Perhaps try a different learning resource such as "Rust By Example"[2], instead?

[1] https://doc.rust-lang.org/std/rc/struct.Rc.html

[2] https://doc.rust-lang.org/rust-by-example/std/rc.html

Reference counting and locks often is the easy path in Rust. It may not feel like it because of the syntax overhead, but I firmly believe it should be one of the first solutions on the list, not a last resort. People get way too fixed on trying to prove to the borrow checker that something or another is OK, because they feel like they need to make things fast, but it's rare that the overhead is actually relevant.
If it's syntactically messy, though, it's not really the easy path. Ergonomics matter just as much as semantics.

I do think that a superset of Rust that provided first-class native syntax for ARC would be much more popular.

Yes! Thank you! Dunno what it is about Rust that makes everyone forget what premature optimization is the root of all of.
The zero cost abstraction is so tantalizingly close enough to reach!

I tell everybody to .clone() and (a)rc away and optimize later. But I often struggle to do that myself ;)

I strongly disagree that smart pointers are "off the paved road". I don't even care to make specific arguments against that notion, it's just a terrible take.
It's telling people to avoid the famously hard meme-road.

Mutexes and reference counting work fine, and are sometimes dramatically simpler than getting absolutely-minimal locks like people seem to always want to do with Rust.

This is what Swift does, and it has even lower performance than tracing GC.

(To be clear, using RC for everything is fine for prototype-level or purely exploratory code, but if you care about performance you'll absolutely want to have good support for non-refcounted objects, as in Rust.)

An interesting point, but I would have to see some very serious performance benchmarks focused specifically on, say, RC Rust vs. GC Golang in order to entertain the notion that an RC PL might be slower than a GC PL. Swift isn't, AFAIK, a good yardstick of... anything in particular, really ;) J/K. Overall PL performance is not only dependent on its memory management, but also on the quality of its standard library and its larger ecosystem, etc.
Can you help me understand when to use Rc<T> instead of Arc<T> (atomic reference counter)?

Edit: Googled it. Found an answer:

> The only distinction between Arc and Rc is that the former is very slightly more expensive, but the latter is not thread-safe.

The distinction between `Rc<T>` and `Arc<T>` exists in the Rust world only to allow the Rust compiler to actually REFUSE to even COMPILE a program that uses a non- thread-safe primitive such as a non-atomic (thus susceptible to thread race conditions) reference-counted smart pointer `Rc<T>` with thread-bound API such as `thread::spawn()`. (Think 1-AM-copy-and-paste from single-threaded codebase into multi-threaded codebase that crashes or leaks memory 3 days later.) Otherwise, `Rc<T>`[1] and `Arc<T>`[2] achieve the same goal. As a general rule, many Rust interfaces exist solely for the purpose of eliminating the possibility of particular mistakes; for example, `Mutex<T>` `lock()`[3] is an interesting one.

[1] https://doc.rust-lang.org/rust-by-example/std/rc.html

[2] https://doc.rust-lang.org/rust-by-example/std/arc.html

[3] https://doc.rust-lang.org/std/sync/struct.Mutex.html

An Arc is an Rc that uses an atomic integer for its ref count. This ensures updates to its count are safe between threads, so the Arc can thus be shared between threads. In practice the two become identical assembly, at least on amd64 because most load and stores have memory ordering guarantees, but on other architectures the atomics can in fact be a tad slower. marking the operations as atomic also prevents the compiler from doing instruction reordering that might cause problems