However, 8 years later I did eventually come to love rust after learning the One Weird Trick of using indices instead of pointers.
However, 8 years later I did eventually come to love rust after learning the One Weird Trick of using indices instead of pointers.
IMvHO, Rust Ownership and Lifetime rules aren't really that hard to learn. The only trick is that a programmer cannot learn Rust exclusively by trial-and-error (which programmers love to do), trying dozens of syntax combinations to see what works. A programmer is forced to learn Rust by RTFM (which programmers hate to do), in order to understand how Rust works. That's all there's to it. It's like trying to learn to play the guitar without reading a book on guitar cords, pulling guitar strings individually at random - the music just won't happen for quite a long time, unless you're Mozart. At any rate, one can always forget Rust references (i.e., raw pointers) and use Rust reference-counted smart pointers[1], effectively writing Rust analogously to Swift.
Having seen that kind of opinion stated elsewhere, it seems what most people would like is rust minus borrowing, and I feel I would get behind that too.
It's already possible. Use Rust reference-counted smart pointers[1] for shareable immutable references and internal mutability[2] for non-shareable mutable references checked at runtime instead of compile time.
[1] https://doc.rust-lang.org/book/ch15-04-rc.html
[2] https://doc.rust-lang.org/book/ch15-05-interior-mutability.h...
let lock = Arc::new(RwLock::new(1));
This plus a few unwrapping things is enough to get you almost std::shared_ptr and then you can fight the borrow checker some other day.
This is probably because using internal mutability not to achieve a valid design goal (such as a controlled side-effect on an otherwise immutable entity), but to side-step the borrow checker for ease of programming, is not considered idiomatic Rust and even though it makes a good educational tool, it should rather not end up in production codebases.
Firstly, when using regular references, you cannot create multiple mutable references. This rule prevents subtle bugs such as data races. When using internal mutability, you still keep that protection, but it is (undesirably) delayed from compile time to runtime, potentially causing an unavoidable crash/panic.
Secondly, when using regular references, you cannot create even a single mutable reference when an immutable reference already exists. This rule prevents subtle bugs such as unexpected mutation ("from under you") of data that was passed-in as immutable. When using internal mutability, you throw away that protection, since multiple immutable reference owners can request a mutable reference (even if only one at a time).
use std::rc::Rc;
use std::cell::RefCell;
let entity1 = Rc::new(RefCell::new(42));
let /* immutable */ entity2 = entity1.clone();
*entity1.borrow_mut() += 27;
assert_ne!(*entity2.borrow_mut(), 42);I have never written a game before, so Catherine West[1] might have very good reasons for choosing ECS, but I... am not so crazy about it. ECS seems to replace Rust references (raw pointers) and/or Rust smart pointers with indexes into one, large "registry" (a container: e.g., a `Vec<T>`) of entities (e.g., `struct` instances). In other words, instead of allowing a Rust pointer (a managed memory address) to keep a piece of memory alive, ECS chooses to have a "registry" keep a piece of memory alive under a particular index, managing allocation and deallocation manually.
In a sense, ECS dumps Rust memory management in favor of... writing the good, ol', data-and-function -oriented C. Quite needlessly (?), since the same (?) can probably be accomplished with Rust reference counted pointers[2], weak pointers[3] and interior mutability[4].
----- CUT HERE -----
use std::rc::{Rc, Weak};
use std::cell::RefCell;
type WeakMutEntity = Weak<RefCell<Entity>>;
struct Entity {
related: WeakMutEntity,
}
impl Entity {
fn new(related: WeakMutEntity) -> Self {
Self { related }
}
fn use_related(&mut self) {
let Some(related) = self.related.upgrade() else { return; };
related.borrow_mut().use_related();
}
}
let entity1 = Rc::new(RefCell::new(
Entity::new(/* null */ Weak::new())));
let entity2 = Rc::new(RefCell::new(
Entity::new(Rc::downgrade(&entity1))));
entity2.borrow_mut().use_related();
----- CUT HERE -----If the above does not get the job done, the solution shouldn't be to just abandon the Rust borrow checker; the solution should be to get the Rust Gods to optimize the available pointer types syntax-wise and/or performance-wise.
[1] https://www.youtube.com/watch?v=aKLntZcp27M
[2] https://doc.rust-lang.org/book/ch15-04-rc.html
[3] https://doc.rust-lang.org/book/ch15-06-reference-cycles.html
[4] https://doc.rust-lang.org/book/ch15-05-interior-mutability.h...
No construction made up of any kind of pointers can achieve that, unless there's a Sufficiently Smart compiler that can magically fully eliminate these pointers.