If you’re going to use (much more expensive) ref-counted heap objects instead of direct references, you might as well be using Swift. (Or Nim, or one of the other new-ish native-compiling languages.) Rust’s competitive advantage is the borrow checker and the way it lets you use direct pointers safely.
The author should at least have pointed out that there’s a significant runtime overhead to using their recommended technique.
No, this a fatalistic take almost like “if you use dyn you may as well use Python”.
Swift’s refcounting is always atomic, but Rust can also use faster non-atomic Rc. Swift has a few local cases where it can omit redundant refcounts, but Rust can borrow Rc‘s content and avoid all refcounts within a scope, even if object’s usage is complex, and that’s a guarantee not dependent on a Sufficiently Smart Compiler.
Swift doesn’t mind doing implicit heap allocations, and all class instances are heap-allocated. Rust doesn’t allocate implicitly and can keep more things on the stack. Swift uses dynamic dispatch quite often, even in basic data structures like strings. In Rust direct inlineable code is the norm, and monomorphisation is a guarantee, even across libraries.
So there’s still a lot more to Rust, even if you need to use Arc in a few places.
Uhu. It seems to me that there are two schools of thought here.
One says: .clone(), Rc and RefCell to make life easier.
The other says: the zen of Rust is ownership: if you express a problem as a tree with clear ownership semantics, then the architecture of your entire application becomes radically simpler. Not every problem has clean ownership mapping, but most problems do, even if it might not be obvious for the start.
I don’t know what approach is better for learning Rust. For writing large-scale production apps, I rather strongly feel that the second one is superior. Arcs and Mutexes make the code significantly harder to understand. The last example, a struct where every filed is an Arc, is a code smell to me: I always try to push arcs outwards in such cases, and have an Arc of struct rather than a struct of arcs.
It’s not that every Arc and mutex is a code smell: on the contrary, there’s usually a couple of Arcs and Mutexes at the top level which are the linch-pin of the whole architecture. Like, the whole rust-analyzer is basically an Arc<RwLock<GlobalState>> plus cancellation. But just throwing arcs and interior mutability everywhere makes it harder to note these central pieces of state management.
I’ve always felt that the order of preference for new code is:
(some people may choose to wedge in “make it correct” somewhere there, but I think that’s either mostly a pipe dream or already part of 1.)
That would mean that you always use the easiest possible techniques in phases 1 and 2 and in phase 3 do something more clever but only if the easy techniques turned out to be a bottleneck.
I’m guessing the easiest technique in Rust terms would be copying a lot.
I tend to agree about that ordering, but I’ve also found that heap allocation and copying is frequently a bottleneck, so much so that I keep it in mind even in steps 1-2. (Of course this applies to pretty low-level performance sensitive code, but that’s the kind of domain Rust gets used for.)
I completely agree. If you don’t need precise control over memory, including the ability to pass around refs to memory safely, then the sane choice is to use a well-designed garbage collected language.
Maybe you’re building something where half needs to control memory and the other half doesn’t. I guess something like this could make sense then.
Swift isn’t exactly “available” on many Linux distributions due to its overengineered build system. The same goes for dotnet. Both of these languages are extremely fickle and run many versions behind the latest stable release offered on the natively supported OS (macOS for Swift and Windows for dotnet).
To build Rust is comparatively sane and a breath of fresh air.
Well, there is OCaml of course. On Linux with a reasonable machine, compiling it from scratch with a C toolchain should take just a handful of minutes. Of course, setting up the OCaml platform tools like opam, dune, etc., will take a few minutes more.
This is my opinion as just another Rust programmer in the community, but: encouraging novices to opt into runtime lifetime management and interior mutability sets them up for failure when they eventually have to interact with the majority of the Rust ecosystem. They’re also a performance and abstraction hazard: a novice who leans too heavily on interior mutability will end up designing interfaces that don’t match impedance with the rest of the crate ecosystem.
These techniques are vital to Rust’s ability to express complex memory semantics, but they’re also a barrier to fully understanding the language when you’re a novice. I suspect that the advice in this post would have been a significant impediment to my personal understanding of the language when I was just starting out.
Rust novices typically mistake using data “by reference” with using Rust references. These are not the same thing! Rust references are temporary views of data, and that has important semantic side effects.
Don’t forget that Rust has Box and Vec that store data “by reference”, but aren’t borrowing the data, so they don’t require lifetime annotations. Box<Foo> has identical in-memory representation as &Foo — both are pointers referencing the data, but they differ in ownership.
Other languages use the term “reference” to mean “not copied”, but Rust uses it to mean “not owned” (not stored here). The “not copied” types don’t have any special names or syntax (mainly because the defaults of copying are flipped, and instead of preventing copies, you need to call .clone() to copy non-trivial data).